@brightspace-ui/core 3.316.0 → 3.317.1

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,24 +1,17 @@
1
1
  import '../colors/colors.js';
2
2
  import '../icons/icon.js';
3
- import './tab-internal.js';
4
- import { css, html, LitElement, nothing, unsafeCSS } from 'lit';
3
+ import { css, html, LitElement, unsafeCSS } from 'lit';
5
4
  import { findComposedAncestor, getOffsetParent, isVisible } from '../../helpers/dom.js';
6
5
  import { getFocusPseudoClass, getFocusRingStyles } from '../../helpers/focus.js';
7
6
  import { ArrowKeysMixin } from '../../mixins/arrow-keys/arrow-keys-mixin.js';
8
7
  import { bodyCompactStyles } from '../typography/styles.js';
9
8
  import { classMap } from 'lit/directives/class-map.js';
10
- import { getFlag } from '../../helpers/flags.js';
11
9
  import { getOverflowDeclarations } from '../../helpers/overflow.js';
12
10
  import { ifDefined } from 'lit/directives/if-defined.js';
13
11
  import { LocalizeCoreElement } from '../../helpers/localize-core-element.js';
14
- import { repeat } from 'lit/directives/repeat.js';
15
12
  import { SkeletonMixin } from '../skeleton/skeleton-mixin.js';
16
13
  import { styleMap } from 'lit/directives/style-map.js';
17
14
 
18
- export function getUseNewTabsStructureFlag() {
19
- return getFlag('GAUD-8299-core-tabs-use-new-structure', true);
20
- }
21
-
22
15
  const reduceMotion = matchMedia('(prefers-reduced-motion: reduce)').matches;
23
16
 
24
17
  const scrollButtonWidth = 56;
@@ -30,7 +23,6 @@ function getOffsetLeft(tab, tabRect) {
30
23
 
31
24
  /**
32
25
  * A component for tabbed content. It supports the "d2l-tab" component and "TabMixin" consumers for tabs, the "d2l-tab-panel" component for the tab content, renders tabs responsively, and provides virtual scrolling for large tab lists.
33
- * @slot - DEPRECATED: Contains the tab panels (e.g., "d2l-tab-panel" components)
34
26
  * @slot ext - Additional content (e.g., a button) positioned at right
35
27
  * @slot tabs - Contains the tabs (e.g., "d2l-tab" components or custom components that use `TabMixin`)
36
28
  * @slot panels - Contains the tab panels (e.g., "d2l-tab-panel" components)
@@ -50,11 +42,9 @@ class Tabs extends LocalizeCoreElement(ArrowKeysMixin(SkeletonMixin(LitElement))
50
42
  text: { type: String },
51
43
  _allowScrollNext: { type: Boolean },
52
44
  _allowScrollPrevious: { type: Boolean },
53
- _defaultSlotBehavior: { state: true },
54
45
  _maxWidth: { type: Number },
55
46
  _scrollCollapsed: { type: Boolean },
56
47
  _state: { type: String },
57
- _tabInfos: { type: Array },
58
48
  _translationValue: {}
59
49
  };
60
50
 
@@ -169,12 +159,10 @@ class Tabs extends LocalizeCoreElement(ArrowKeysMixin(SkeletonMixin(LitElement))
169
159
  transition: margin-top 200ms ease-out;
170
160
  }
171
161
 
172
- d2l-tab-internal, ::slotted([role="tab"]) {
162
+ ::slotted([role="tab"]) {
173
163
  -webkit-transition: max-width 200ms ease-out, opacity 200ms ease-out, transform 200ms ease-out;
174
164
  transition: max-width 200ms ease-out, opacity 200ms ease-out, transform 200ms ease-out;
175
165
  }
176
- d2l-tab-internal[data-state="adding"],
177
- d2l-tab-internal[data-state="removing"],
178
166
  ::slotted([role="tab"][data-state="adding"]),
179
167
  ::slotted([role="tab"][data-state="removing"]) {
180
168
  max-width: 0;
@@ -200,7 +188,7 @@ class Tabs extends LocalizeCoreElement(ArrowKeysMixin(SkeletonMixin(LitElement))
200
188
  -webkit-transition: none;
201
189
  transition: none;
202
190
  }
203
- d2l-tab-internal, ::slotted([role="tab"]) {
191
+ ::slotted([role="tab"]) {
204
192
  -webkit-transition: none;
205
193
  transition: none;
206
194
  }
@@ -229,21 +217,13 @@ class Tabs extends LocalizeCoreElement(ArrowKeysMixin(SkeletonMixin(LitElement))
229
217
  this.maxToShow = -1;
230
218
  this._allowScrollNext = false;
231
219
  this._allowScrollPrevious = false;
232
-
233
- /*
234
- * Remove this._defaultSlotBehavior and related code with GAUD-8299-core-tabs-use-new-structure flag clean up
235
- * NOTE: remove the TRUE case of _defaultSlotBehavior
236
- */
237
- this._defaultSlotBehavior = !this.#newTabsPanelStructure;
238
-
239
- this._loadingCompleteResolve = undefined;
240
- this._loadingCompletePromise = new Promise(resolve => this._loadingCompleteResolve = resolve);
220
+ this.#loadingCompleteResolve = undefined;
221
+ this.#loadingCompletePromise = new Promise(resolve => this.#loadingCompleteResolve = resolve);
241
222
  this._maxWidth = null;
242
223
  this._scrollCollapsed = false;
243
224
  this._state = 'shown';
244
- this._tabIds = {};
245
- this._tabs = [];
246
- if (this._defaultSlotBehavior) this._tabInfos = [];
225
+ this.#tabIds = {};
226
+ this.#tabs = [];
247
227
  this._translationValue = 0;
248
228
  }
249
229
 
@@ -251,7 +231,7 @@ class Tabs extends LocalizeCoreElement(ArrowKeysMixin(SkeletonMixin(LitElement))
251
231
  super.connectedCallback();
252
232
 
253
233
  queueMicrotask(() => {
254
- const bgColor = this._getComputedBackgroundColor();
234
+ const bgColor = this.#getComputedBackgroundColor();
255
235
  if (bgColor && bgColor !== 'rgb(255, 255, 255)' && bgColor !== 'rgba(255, 255, 255, 1)') {
256
236
  this.style.setProperty('--d2l-tabs-background-color', bgColor);
257
237
  }
@@ -261,75 +241,45 @@ class Tabs extends LocalizeCoreElement(ArrowKeysMixin(SkeletonMixin(LitElement))
261
241
 
262
242
  disconnectedCallback() {
263
243
  super.disconnectedCallback();
264
- if (this._resizeObserver) this._resizeObserver.disconnect();
244
+ if (this.#resizeObserver) this.#resizeObserver.disconnect();
265
245
  }
266
246
 
267
247
  firstUpdated(changedProperties) {
268
248
  super.firstUpdated(changedProperties);
269
249
 
270
250
  this.arrowKeysFocusablesProvider = async() => {
271
- return this._defaultSlotBehavior ? [...this.shadowRoot.querySelectorAll('d2l-tab-internal')] : this._tabs;
251
+ return this.#tabs;
272
252
  };
273
253
 
274
254
  this.arrowKeysOnBeforeFocus = async(tab) => {
275
- if (this._defaultSlotBehavior) {
276
- // remove this section with GAUD-8299-core-tabs-use-new-structure flag clean up
277
- const tabInfo = this._getTabInfo(tab.controlsPanel);
278
- this._setFocusableDefaultSlotBehavior(tabInfo);
255
+ this.#setFocusable(tab);
279
256
 
280
- this.requestUpdate();
281
- await this.updateComplete;
257
+ this.requestUpdate();
258
+ await this.updateComplete;
282
259
 
283
- if (!this._scrollCollapsed) {
284
- return this._updateScrollPositionDefaultSlotBehavior(tabInfo);
285
- } else {
286
- const measures = this._getMeasures();
287
- const newTranslationValue = this._calculateScrollPositionDefaultSlotBehavior(tabInfo, measures);
288
-
289
- if (!this.#isRTL()) {
290
- if (newTranslationValue >= 0) return;
291
- } else {
292
- if (newTranslationValue <= 0) return;
293
- }
294
-
295
- const expanded = await this._tryExpandTabsContainer(measures);
296
- if (expanded) {
297
- return;
298
- } else {
299
- return this._updateScrollPositionDefaultSlotBehavior(tabInfo);
300
- }
301
- }
260
+ if (!this._scrollCollapsed) {
261
+ return this.#updateScrollPosition(tab);
302
262
  } else {
303
- this._setFocusable(tab);
263
+ const measures = this.#getMeasures();
264
+ const newTranslationValue = this.#calculateScrollPosition(tab, measures);
304
265
 
305
- this.requestUpdate();
306
- await this.updateComplete;
266
+ if (!this.#isRTL()) {
267
+ if (newTranslationValue >= 0) return;
268
+ } else {
269
+ if (newTranslationValue <= 0) return;
270
+ }
307
271
 
308
- if (!this._scrollCollapsed) {
309
- return this._updateScrollPosition(tab);
272
+ const expanded = await this.#tryExpandTabsContainer(measures);
273
+ if (expanded) {
274
+ return;
310
275
  } else {
311
- const measures = this._getMeasures();
312
- const newTranslationValue = this._calculateScrollPosition(tab, measures);
313
-
314
- if (!this.#isRTL()) {
315
- if (newTranslationValue >= 0) return;
316
- } else {
317
- if (newTranslationValue <= 0) return;
318
- }
319
-
320
- const expanded = await this._tryExpandTabsContainer(measures);
321
- if (expanded) {
322
- return;
323
- } else {
324
- return this._updateScrollPosition(tab);
325
- }
276
+ return this.#updateScrollPosition(tab);
326
277
  }
327
278
  }
328
279
  };
329
280
 
330
- this._handleResize = this._handleResize.bind(this);
331
- this._resizeObserver = new ResizeObserver(this._handleResize);
332
- this._resizeObserver.observe(this.shadowRoot.querySelector('.d2l-tabs-container-list'));
281
+ this.#resizeObserver = new ResizeObserver(this.#handleResize);
282
+ this.#resizeObserver.observe(this.shadowRoot.querySelector('.d2l-tabs-container-list'));
333
283
 
334
284
  }
335
285
 
@@ -362,36 +312,27 @@ class Tabs extends LocalizeCoreElement(ArrowKeysMixin(SkeletonMixin(LitElement))
362
312
  style="${styleMap(tabsContainerStyles)}">
363
313
  <div class="d2l-tabs-scroll-previous-container">
364
314
  <button class="d2l-tabs-scroll-button"
365
- @click="${this._handleScrollPrevious}"
315
+ @click="${this.#handleScrollPrevious}"
366
316
  title="${this.localize('components.tabs.previous')}">
367
317
  <d2l-icon icon="tier1:chevron-left"></d2l-icon>
368
318
  </button>
369
319
  </div>
370
320
  ${this.arrowKeysContainer(html`
371
321
  <div class="d2l-tabs-container-list"
372
- @d2l-tab-content-change="${this._handleTabContentChange}"
322
+ @d2l-tab-content-change="${this.#handleTabContentChange}"
373
323
  @d2l-tab-hidden-change="${this.#handleTabHiddenChange}"
374
- @d2l-tab-selected="${this._handleTabSelected}"
324
+ @d2l-tab-selected="${this.#handleTabSelected}"
375
325
  @d2l-tab-deselected="${this.#handleTabDeselected}"
376
- @focusout="${this._handleFocusOut}"
326
+ @focusout="${this.#handleFocusOut}"
377
327
  aria-label="${ifDefined(this.text)}"
378
328
  role="tablist"
379
329
  style="${styleMap(tabsContainerListStyles)}">
380
- ${!this.#newTabsPanelStructure ? repeat(this._tabInfos, (tabInfo) => tabInfo.id, (tabInfo) => html`
381
- <d2l-tab-internal aria-selected="${tabInfo.selected ? 'true' : 'false'}"
382
- .controlsPanel="${tabInfo.id}"
383
- data-state="${tabInfo.state}"
384
- ?skeleton="${this.skeleton}"
385
- tabindex="${tabInfo.activeFocusable ? 0 : -1}"
386
- text="${tabInfo.text}">
387
- </d2l-tab-internal>
388
- `) : nothing}
389
- <slot name="tabs" @slotchange="${this._handleTabsSlotChange}"></slot>
330
+ <slot name="tabs" @slotchange="${this.#handleTabsSlotChange}"></slot>
390
331
  </div>
391
332
  `)}
392
333
  <div class="d2l-tabs-scroll-next-container">
393
334
  <button class="d2l-tabs-scroll-button"
394
- @click="${this._handleScrollNext}"
335
+ @click="${this.#handleScrollNext}"
395
336
  title="${this.localize('components.tabs.next')}">
396
337
  <d2l-icon icon="tier1:chevron-right"></d2l-icon>
397
338
  </button>
@@ -399,21 +340,18 @@ class Tabs extends LocalizeCoreElement(ArrowKeysMixin(SkeletonMixin(LitElement))
399
340
  </div>
400
341
  <div class="d2l-tabs-container-ext"><slot name="ext"></slot></div>
401
342
  </div>
402
- <div class="${classMap(panelContainerClasses)}"
403
- @d2l-tab-panel-selected="${ifDefined(!this.#newTabsPanelStructure ? this._handlePanelSelected : undefined)}"
404
- @d2l-tab-panel-text-changed="${ifDefined(!this.#newTabsPanelStructure ? this._handlePanelTextChange : undefined)}">
405
- ${!this.#newTabsPanelStructure ? html`<slot @slotchange="${this._handleDefaultSlotChange}"></slot>` : nothing}
406
- <slot name="panels" @slotchange="${this._handlePanelsSlotChange}"></slot>
343
+ <div class="${classMap(panelContainerClasses)}">
344
+ <slot name="panels" @slotchange="${this.#handlePanelsSlotChange}"></slot>
407
345
  </div>
408
346
  `;
409
347
  }
410
348
 
411
349
  focus() {
412
- return this._focusSelected();
350
+ return this.#focusSelected();
413
351
  }
414
352
 
415
353
  async getLoadingComplete() {
416
- return this._loadingCompletePromise;
354
+ return this.#loadingCompletePromise;
417
355
  }
418
356
 
419
357
  getTabListRect() {
@@ -423,15 +361,28 @@ class Tabs extends LocalizeCoreElement(ArrowKeysMixin(SkeletonMixin(LitElement))
423
361
 
424
362
  hideTab(tab) {
425
363
  tab.setAttribute('data-state', 'removing');
426
- return (Object.keys(this._tabIds).length > 1 && !reduceMotion) ? this._animateTabRemoval(tab) : Promise.resolve();
364
+ return (Object.keys(this.#tabIds).length > 1 && !reduceMotion) ? this.#animateTabRemoval(tab) : Promise.resolve();
427
365
  }
428
366
 
429
367
  #checkTabPanelMatchRequested;
430
- #newTabsPanelStructure = getUseNewTabsStructureFlag();
368
+ #initialized;
369
+ #loadingCompletePromise;
370
+ #loadingCompleteResolve;
371
+ #measures;
431
372
  #panels;
373
+ #resizeObserver;
374
+ #tabIds;
375
+ #tabs;
432
376
  #updateAriaControlsRequested;
433
377
 
434
- _animateTabAddition(tab) {
378
+ #handleResize = (entries) => {
379
+ const measures = this.#getMeasures();
380
+ if (entries.length === 1 && entries[0].contentRect.width === measures.tabsContainerListRect.width) return;
381
+ this.#updateMeasures();
382
+ this.#updateScrollVisibility(this.#getMeasures());
383
+ };
384
+
385
+ #animateTabAddition(tab) {
435
386
  if (!tab || reduceMotion) {
436
387
  return new Promise((resolve) => {
437
388
  tab.setAttribute('data-state', '');
@@ -452,25 +403,7 @@ class Tabs extends LocalizeCoreElement(ArrowKeysMixin(SkeletonMixin(LitElement))
452
403
  });
453
404
  }
454
405
 
455
- // remove with GAUD-8299-core-tabs-use-new-structure flag clean up
456
- _animateTabAdditionDefaultSlotBehavior(tabInfo) {
457
- const tab = this.shadowRoot
458
- && this.shadowRoot.querySelector(`d2l-tab-internal[controls-panel="${CSS.escape(tabInfo.id)}"]`);
459
- if (!tab) Promise.resolve();
460
-
461
- return new Promise((resolve) => {
462
- const handleTransitionEnd = (e) => {
463
- if (e.propertyName !== 'max-width') return;
464
- tab.removeEventListener('transitionend', handleTransitionEnd);
465
- resolve();
466
- };
467
- tab.addEventListener('transitionend', handleTransitionEnd);
468
- tabInfo.state = '';
469
- this.requestUpdate();
470
- });
471
- }
472
-
473
- _animateTabRemoval(tab) {
406
+ #animateTabRemoval(tab) {
474
407
  if (!tab || reduceMotion) return Promise.resolve();
475
408
 
476
409
  return new Promise((resolve) => {
@@ -484,62 +417,120 @@ class Tabs extends LocalizeCoreElement(ArrowKeysMixin(SkeletonMixin(LitElement))
484
417
  });
485
418
  }
486
419
 
487
- // remove with GAUD-8299-core-tabs-use-new-structure flag clean up
488
- _animateTabRemovalDefaultSlotBehavior(tabInfo) {
489
- const tab = this.shadowRoot &&
490
- this.shadowRoot.querySelector(`d2l-tab-internal[controls-panel="${CSS.escape(tabInfo.id)}"]`);
491
- if (!tab) Promise.resolve();
492
-
493
- return new Promise((resolve) => {
494
- const handleTransitionEnd = (e) => {
495
- if (e.propertyName !== 'max-width') return;
496
- tab.removeEventListener('transitionend', handleTransitionEnd);
497
- this._tabInfos.splice(this._tabInfos.findIndex(info => info.id === tabInfo.id), 1);
498
- this.requestUpdate();
499
- resolve();
500
- };
501
- tab.addEventListener('transitionend', handleTransitionEnd);
502
- });
503
- }
504
-
505
- _calculateScrollPosition(selectedTab, measures) {
506
- const tabs = this._tabs;
420
+ #calculateScrollPosition(selectedTab, measures) {
421
+ const tabs = this.#tabs;
507
422
  const selectedTabIndex = tabs.indexOf(selectedTab);
508
423
  return this.#calculateScrollPositionLogic(tabs, selectedTabIndex, measures);
509
424
  }
510
425
 
511
- // remove with GAUD-8299-core-tabs-use-new-structure flag clean up
512
- _calculateScrollPositionDefaultSlotBehavior(selectedTabInfo, measures) {
513
- const selectedTabIndex = this._tabInfos.indexOf(selectedTabInfo);
514
- return this.#calculateScrollPositionLogic(this._tabInfos, selectedTabIndex, measures);
515
- }
426
+ #calculateScrollPositionLogic(tabsDataStructure, selectedTabIndex, measures) {
427
+ if (!measures.tabRects[selectedTabIndex]) return 0;
428
+
429
+ const selectedTabMeasures = measures.tabRects[selectedTabIndex];
430
+
431
+ const isOverflowingLeft = (selectedTabMeasures.offsetLeft + this._translationValue < 0);
432
+ const isOverflowingRight = (selectedTabMeasures.offsetLeft + selectedTabMeasures.rect.width + this._translationValue > measures.tabsContainerRect.width);
433
+
434
+ const isRTL = this.#isRTL();
435
+
436
+ let getNewTranslationValue;
437
+ if (!isRTL) {
438
+ getNewTranslationValue = () => {
439
+ if (selectedTabIndex === 0) {
440
+ // position selected tab at beginning
441
+ return 0;
442
+ } else if (selectedTabIndex === (tabsDataStructure.length - 1)) {
443
+ // position selected tab at end
444
+ return -1 * (selectedTabMeasures.offsetLeft - measures.tabsContainerRect.width + selectedTabMeasures.rect.width);
445
+ } else {
446
+ // position selected tab in middle
447
+ return -1 * (selectedTabMeasures.offsetLeft - (measures.tabsContainerRect.width / 2) + (selectedTabMeasures.rect.width / 2));
448
+ }
449
+ };
450
+ } else {
451
+ getNewTranslationValue = () => {
452
+ if (selectedTabIndex === 0) {
453
+ // position selected tab at beginning
454
+ return 0;
455
+ } else if (selectedTabIndex === (tabsDataStructure.length - 1)) {
456
+ // position selected tab at end
457
+ return -1 * selectedTabMeasures.offsetLeft;
458
+ } else {
459
+ // position selected tab in middle
460
+ return (measures.tabsContainerRect.width / 2) - (selectedTabMeasures.offsetLeft + selectedTabMeasures.rect.width / 2) + (selectedTabMeasures.rect.width / 2);
461
+ }
462
+ };
463
+ }
516
464
 
517
- async _focusSelected() {
518
- if (this._defaultSlotBehavior) {
519
- this._focusSelectedDefaultSlotBehavior();
520
- return;
465
+ let newTranslationValue = this._translationValue;
466
+ if (isOverflowingLeft || isOverflowingRight) {
467
+ newTranslationValue = getNewTranslationValue();
521
468
  }
522
469
 
523
- const selectedTab = this._tabs.find(ti => ti.selected);
524
- if (!selectedTab) return;
470
+ let expectedPosition;
471
+
472
+ // make sure the new position will not place selected tab behind left scroll button
473
+ if (!isRTL) {
474
+ expectedPosition = selectedTabMeasures.offsetLeft + newTranslationValue;
475
+ if (newTranslationValue < 0 && this.#isPositionInLeftScrollArea(expectedPosition)) {
476
+ newTranslationValue = getNewTranslationValue();
477
+ }
478
+ } else {
479
+ expectedPosition = selectedTabMeasures.offsetLeft + selectedTabMeasures.rect.width + newTranslationValue;
480
+ if (newTranslationValue > 0 && this.#isPositionInRightScrollArea(expectedPosition, measures)) {
481
+ newTranslationValue = getNewTranslationValue();
482
+ }
483
+ }
525
484
 
526
- await this._updateScrollPosition(selectedTab);
485
+ if (!isRTL) {
486
+ // make sure there will not be any empty space between left side of container and first tab
487
+ if (newTranslationValue > 0) newTranslationValue = 0;
488
+ } else {
489
+ // make sure there will not be any empty space between right side of container and first tab
490
+ if (newTranslationValue < 0) newTranslationValue = 0;
491
+ }
527
492
 
528
- selectedTab.focus();
493
+ // make sure the new position will not place selected tab behind the right scroll button
494
+ if (!isRTL) {
495
+ expectedPosition = selectedTabMeasures.offsetLeft + selectedTabMeasures.rect.width + newTranslationValue;
496
+ if ((selectedTabIndex < tabsDataStructure.length - 1) && this.#isPositionInRightScrollArea(expectedPosition, measures)) {
497
+ newTranslationValue = getNewTranslationValue();
498
+ }
499
+ } else {
500
+ expectedPosition = selectedTabMeasures.offsetLeft + newTranslationValue;
501
+ if ((selectedTabIndex < tabsDataStructure.length - 1) && this.#isPositionInLeftScrollArea(expectedPosition)) {
502
+ newTranslationValue = getNewTranslationValue();
503
+ }
504
+ }
505
+
506
+ return newTranslationValue;
507
+ }
508
+
509
+ #checkTabPanelMatch() {
510
+ // debounce so only runs once when tabs/panels slots changing
511
+ if (this.#checkTabPanelMatchRequested) return;
512
+
513
+ this.#checkTabPanelMatchRequested = true;
514
+ setTimeout(() => {
515
+ if ((this.#tabs && !this.#panels) || (this.#panels && !this.#tabs)) {
516
+ console.warn('d2l-tabs: tabs and panels are not in sync');
517
+ } else if (this.#tabs.length !== this.#panels.length) {
518
+ console.warn('d2l-tabs: number of tabs and panels does not match');
519
+ }
520
+ this.#checkTabPanelMatchRequested = false;
521
+ }, 0);
529
522
  }
530
523
 
531
- // remove with GAUD-8299-core-tabs-use-new-structure flag clean up
532
- async _focusSelectedDefaultSlotBehavior() {
533
- const selectedTab = this.shadowRoot && this.shadowRoot.querySelector('d2l-tab-internal[aria-selected="true"]');
524
+ async #focusSelected() {
525
+ const selectedTab = this.#tabs.find(ti => ti.selected);
534
526
  if (!selectedTab) return;
535
527
 
536
- const selectedTabInfo = this._getTabInfo(selectedTab.controlsPanel);
537
- await this._updateScrollPositionDefaultSlotBehavior(selectedTabInfo);
528
+ await this.#updateScrollPosition(selectedTab);
538
529
 
539
530
  selectedTab.focus();
540
531
  }
541
532
 
542
- _getComputedBackgroundColor() {
533
+ #getComputedBackgroundColor() {
543
534
  let bgColor = null;
544
535
 
545
536
  findComposedAncestor(this, (node) => {
@@ -553,177 +544,33 @@ class Tabs extends LocalizeCoreElement(ArrowKeysMixin(SkeletonMixin(LitElement))
553
544
  return bgColor;
554
545
  }
555
546
 
556
- _getMeasures() {
557
- if (!this._measures) this._updateMeasures();
558
- return this._measures;
547
+ #getMeasures() {
548
+ if (!this.#measures) this.#updateMeasures();
549
+ return this.#measures;
559
550
  }
560
551
 
561
- _getPanel(id) {
562
- if (this._defaultSlotBehavior) return this._getPanelDefaultSlotBehavior(id);
563
-
552
+ #getPanel(id) {
564
553
  if (!this.#panels) return;
565
554
  return this.#panels.find(panel => panel.labelledBy === id);
566
555
  }
567
556
 
568
- // remove with GAUD-8299-core-tabs-use-new-structure flag clean up
569
- _getPanelDefaultSlotBehavior(id) {
570
- if (!this.shadowRoot) return;
571
- // use simple selector for slot (Edge)
572
- const slot = this.shadowRoot.querySelector('.d2l-panels-container').querySelector('slot');
573
- const panels = this._getPanelsDefaultSlotBehavior(slot);
574
- for (let i = 0; i < panels.length; i++) {
575
- if (panels[i].nodeType === Node.ELEMENT_NODE && panels[i].role === 'tabpanel' && panels[i].id === id) {
576
- return panels[i];
577
- }
578
- }
579
- }
580
-
581
- // rremove with GAUD-8299-core-tabs-use-new-structure flag clean up
582
- _getPanelsDefaultSlotBehavior(slot) {
583
- if (!slot) return;
584
- return slot.assignedElements({ flatten: true }).filter((node) => node.role === 'tabpanel');
585
- }
586
-
587
- // remove with GAUD-8299-core-tabs-use-new-structure flag clean up
588
- _getTabInfo(id) {
589
- return this._tabInfos.find((t) => t.id === id);
590
- }
591
-
592
- // remove with GAUD-8299-core-tabs-use-new-structure flag clean up
593
- async _handleDefaultSlotChange(e) {
594
- if (!this._defaultSlotBehavior) return;
595
-
596
- const panels = this._getPanelsDefaultSlotBehavior(e.target);
597
-
598
- // handle case where there are less than two tabs initially
599
- this._updateTabListVisibility(panels);
600
-
601
- if (!this._initialized && panels.length === 0) return;
602
-
603
- let selectedTabInfo = null;
604
-
605
- const newTabInfos = panels.map((panel) => {
606
- let state = '';
607
- if (this._initialized && !reduceMotion && panels.length !== this._tabInfos.length) {
608
- // if it's a new tab, update state to animate addition
609
- if (this._tabInfos.findIndex(info => info.id === panel.id) === -1) {
610
- state = 'adding';
611
- }
612
- }
613
- const tabInfo = {
614
- id: panel.id,
615
- text: panel.text,
616
- selected: panel.selected,
617
- state: state
618
- };
619
- if (tabInfo.selected) {
620
- selectedTabInfo = tabInfo;
621
- this._setFocusableDefaultSlotBehavior(tabInfo);
622
- }
623
- return tabInfo;
624
- });
625
-
626
- if (this._initialized && !reduceMotion && this._tabInfos.length !== newTabInfos.length) {
627
- this._tabInfos.forEach((info, index) => {
628
- // if a tab was removed, include old info to animate it away
629
- if (newTabInfos.findIndex(newInfo => newInfo.id === info.id) === -1) {
630
- info.state = 'removing';
631
- info.selected = false;
632
- newTabInfos.splice(index, 0, info);
633
- }
634
- });
635
- }
636
- this._tabInfos = newTabInfos;
637
-
638
- if (this._tabInfos.length > 0 && !selectedTabInfo) {
639
- selectedTabInfo = this._tabInfos.find(tabInfo => tabInfo.state !== 'removing');
640
- if (selectedTabInfo) {
641
- selectedTabInfo.activeFocusable = true;
642
- selectedTabInfo.selected = true;
643
- }
644
- }
645
-
646
- await this.updateComplete;
647
-
648
- const animPromises = [];
649
- if (!this._initialized && this._tabInfos.length > 0) {
650
-
651
- this._initialized = true;
652
- await this._updateTabsContainerWidthDefaultSlotBehavior(selectedTabInfo);
653
-
654
- } else {
655
-
656
- if (this._tabInfos.length > 1) {
657
- this._tabInfos.forEach((info) => {
658
- if (info.state === 'adding') animPromises.push(this._animateTabAdditionDefaultSlotBehavior(info));
659
- else if (info.state === 'removing') animPromises.push(this._animateTabRemovalDefaultSlotBehavior(info));
660
- });
661
- }
662
-
663
- // required for animation
664
- this._updateMeasures();
665
- }
666
-
667
- if (selectedTabInfo) {
668
- Promise.all(animPromises).then(() => {
669
- this._updateMeasures();
670
- return this._updateScrollPositionDefaultSlotBehavior(selectedTabInfo);
671
- });
672
- }
673
-
674
- }
675
-
676
- _handleFocusOut(e) {
557
+ #handleFocusOut(e) {
677
558
  if (e.relatedTarget && e.relatedTarget.role === 'tab') return;
678
- this._resetFocusables();
559
+ this.#resetFocusables();
679
560
  }
680
561
 
681
- // remove with GAUD-8299-core-tabs-use-new-structure flag clean up
682
- _handlePanelSelected(e) {
683
- if (!this._defaultSlotBehavior) return;
684
-
685
- const tabInfo = this._getTabInfo(e.target.id);
686
- // event could be from nested tabs
687
- if (!tabInfo) return;
688
-
689
- this._setFocusableDefaultSlotBehavior(tabInfo);
690
- tabInfo.selected = true;
691
- this.requestUpdate();
692
- }
693
-
694
- _handlePanelsSlotChange(e) {
695
- if (this._defaultSlotBehavior) return;
696
-
562
+ #handlePanelsSlotChange(e) {
697
563
  this.#panels = e.target.assignedElements({ flatten: true }).filter((node) => node.role === 'tabpanel');
698
564
  this.#checkTabPanelMatch();
699
565
  this.#setAriaControls();
700
566
  }
701
567
 
702
- // remove with GAUD-8299-core-tabs-use-new-structure flag clean up
703
- async _handlePanelTextChange(e) {
704
- const tabInfo = this._getTabInfo(e.target.id);
705
- // event could be from nested tabs
706
- if (!tabInfo) return;
707
- tabInfo.text = e.target.text;
708
- this.requestUpdate();
709
- await this.updateComplete;
710
- this._updateMeasures();
711
- await this._updateScrollVisibility(this._getMeasures());
712
- }
713
-
714
- _handleResize(entries) {
715
- const measures = this._getMeasures();
716
- if (entries.length === 1 && entries[0].contentRect.width === measures.tabsContainerListRect.width) return;
717
- this._updateMeasures();
718
- this._updateScrollVisibility(this._getMeasures());
719
- }
568
+ async #handleScrollNext() {
720
569
 
721
- async _handleScrollNext() {
570
+ const measures = this.#getMeasures();
722
571
 
723
- const measures = this._getMeasures();
724
-
725
- const expanded = await this._tryExpandTabsContainer(measures);
726
- const newMeasures = expanded ? this._getMeasures() : measures;
572
+ const expanded = await this.#tryExpandTabsContainer(measures);
573
+ const newMeasures = expanded ? this.#getMeasures() : measures;
727
574
 
728
575
  let newTranslationValue;
729
576
  const lastTabMeasures = measures.tabRects[measures.tabRects.length - 1];
@@ -753,8 +600,8 @@ class Tabs extends LocalizeCoreElement(ArrowKeysMixin(SkeletonMixin(LitElement))
753
600
 
754
601
  }
755
602
 
756
- await this._scrollToPosition(newTranslationValue);
757
- await this._updateScrollVisibility(newMeasures);
603
+ await this.#scrollToPosition(newTranslationValue);
604
+ await this.#updateScrollVisibility(newMeasures);
758
605
 
759
606
  if (!isOverflowingNext && this.shadowRoot) {
760
607
  this.shadowRoot.querySelector('.d2l-tabs-scroll-previous-container button').focus();
@@ -762,12 +609,12 @@ class Tabs extends LocalizeCoreElement(ArrowKeysMixin(SkeletonMixin(LitElement))
762
609
 
763
610
  }
764
611
 
765
- async _handleScrollPrevious() {
612
+ async #handleScrollPrevious() {
766
613
 
767
- const measures = this._getMeasures();
614
+ const measures = this.#getMeasures();
768
615
 
769
- const expanded = await this._tryExpandTabsContainer(measures);
770
- const newMeasures = expanded ? this._getMeasures() : measures;
616
+ const expanded = await this.#tryExpandTabsContainer(measures);
617
+ const newMeasures = expanded ? this.#getMeasures() : measures;
771
618
 
772
619
  let newTranslationValue;
773
620
  let isOverflowingPrevious;
@@ -786,8 +633,8 @@ class Tabs extends LocalizeCoreElement(ArrowKeysMixin(SkeletonMixin(LitElement))
786
633
 
787
634
  }
788
635
 
789
- await this._scrollToPosition(newTranslationValue);
790
- await this._updateScrollVisibility(newMeasures);
636
+ await this.#scrollToPosition(newTranslationValue);
637
+ await this.#updateScrollVisibility(newMeasures);
791
638
 
792
639
  if (!isOverflowingPrevious && this.shadowRoot) {
793
640
  this.shadowRoot.querySelector('.d2l-tabs-scroll-next-container button').focus();
@@ -795,68 +642,50 @@ class Tabs extends LocalizeCoreElement(ArrowKeysMixin(SkeletonMixin(LitElement))
795
642
 
796
643
  }
797
644
 
798
- async _handleTabContentChange() {
799
- this._updateMeasures();
800
- await this._updateScrollVisibility(this._getMeasures());
645
+ async #handleTabContentChange() {
646
+ this.#updateMeasures();
647
+ await this.#updateScrollVisibility(this.#getMeasures());
801
648
  }
802
649
 
803
- async _handleTabSelected(e) {
804
- if (this._defaultSlotBehavior) {
805
- this._handleTabSelectedDefaultSlotBehavior(e);
806
- return;
807
- }
808
-
809
- const selectedTab = e.target;
810
- this.#updateSelectedTab(selectedTab);
811
- await this.updateComplete;
812
- this._updateScrollPosition(selectedTab);
650
+ #handleTabDeselected(e) {
651
+ const panel = this.#getPanel(e.target.id);
652
+ if (panel) panel._selected = false;
813
653
  }
814
654
 
815
- // remove with GAUD-8299-core-tabs-use-new-structure flag clean up
816
- async _handleTabSelectedDefaultSlotBehavior(e) {
817
- e.stopPropagation();
818
-
819
- const selectedTab = e.target;
820
- const selectedPanel = this._getPanel(selectedTab.controlsPanel);
821
- const selectedTabInfo = this._getTabInfo(selectedTab.controlsPanel);
822
- selectedTabInfo.activeFocusable = true;
655
+ #handleTabHiddenChange() {
656
+ if (!this.#tabs || this.#tabs.length <= 1) return;
823
657
 
824
- await this.updateComplete;
825
- this._updateScrollPositionDefaultSlotBehavior(selectedTabInfo);
826
-
827
- selectedPanel.selected = true;
828
- this._tabInfos.forEach((tabInfo) => {
829
- if (tabInfo.id !== selectedTab.controlsPanel) {
830
- if (tabInfo.selected) {
831
- tabInfo.selected = false;
832
- const panel = this._getPanel(tabInfo.id);
833
- // panel may not exist if it's being removed
834
- if (panel) panel.selected = false;
835
- }
836
- if (tabInfo.activeFocusable) tabInfo.activeFocusable = false;
837
- }
658
+ let visibleTabCount = 0;
659
+ this.#tabs.forEach((tab) => {
660
+ if (!tab.hidden) visibleTabCount++;
838
661
  });
839
662
 
840
- this.requestUpdate();
663
+ if (visibleTabCount > 1 && this._state === 'hidden') this.#showTabsList();
664
+ else if (visibleTabCount <= 1 && this._state === 'shown') this.#hideTabsList();
841
665
  }
842
666
 
843
- async _handleTabsSlotChange(e) {
844
- this._defaultSlotBehavior = false;
667
+ async #handleTabSelected(e) {
668
+ const selectedTab = e.target;
669
+ this.#updateSelectedTab(selectedTab);
670
+ await this.updateComplete;
671
+ this.#updateScrollPosition(selectedTab);
672
+ }
845
673
 
846
- this._tabs = e.target.assignedElements({ flatten: true }).filter((node) => node.role === 'tab');
674
+ async #handleTabsSlotChange(e) {
675
+ this.#tabs = e.target.assignedElements({ flatten: true }).filter((node) => node.role === 'tab');
847
676
 
848
677
  // handle case where there are less than two tabs initially
849
- this._updateTabListVisibility(this._tabs);
678
+ this.#updateTabListVisibility(this.#tabs);
850
679
 
851
- if (!this._initialized && this._tabs.length === 0) return;
680
+ if (!this.#initialized && this.#tabs.length === 0) return;
852
681
 
853
682
  let selectedTab = null;
854
683
  const newTabIds = {};
855
- this._tabs?.forEach((tab) => {
856
- const isNew = this._initialized && !this._tabIds[tab.id];
857
- if (isNew && !reduceMotion && this._tabs.length !== Object.keys(this._tabIds).length) {
684
+ this.#tabs?.forEach((tab) => {
685
+ const isNew = this.#initialized && !this.#tabIds[tab.id];
686
+ if (isNew && !reduceMotion && this.#tabs.length !== Object.keys(this.#tabIds).length) {
858
687
  // if it's a new tab, update state to animate addition
859
- this._tabIds[tab.id] = true;
688
+ this.#tabIds[tab.id] = true;
860
689
  tab.setAttribute('data-state', 'adding');
861
690
  }
862
691
  if (tab.selected && tab.getAttribute('data-state') !== 'removing') {
@@ -866,10 +695,10 @@ class Tabs extends LocalizeCoreElement(ArrowKeysMixin(SkeletonMixin(LitElement))
866
695
  newTabIds[tab.id] = true;
867
696
  });
868
697
 
869
- this._tabIds = newTabIds;
698
+ this.#tabIds = newTabIds;
870
699
 
871
700
  if (!selectedTab) {
872
- selectedTab = this._tabs.find((tab) => tab.getAttribute('data-state') !== 'removing');
701
+ selectedTab = this.#tabs.find((tab) => tab.getAttribute('data-state') !== 'removing');
873
702
  if (selectedTab) selectedTab.selected = true;
874
703
  }
875
704
  if (selectedTab) {
@@ -882,47 +711,62 @@ class Tabs extends LocalizeCoreElement(ArrowKeysMixin(SkeletonMixin(LitElement))
882
711
 
883
712
  const animPromises = [];
884
713
 
885
- if (!this._initialized && this._tabs.length > 0) {
886
- this._initialized = true;
887
- await this._updateTabsContainerWidth(selectedTab);
714
+ if (!this.#initialized && this.#tabs.length > 0) {
715
+ this.#initialized = true;
716
+ await this.#updateTabsContainerWidth(selectedTab);
888
717
  } else {
889
- if (this._tabs.length > 1) {
890
- this._tabs.forEach((tab) => {
891
- if (tab.getAttribute('data-state') === 'adding') animPromises.push(this._animateTabAddition(tab));
718
+ if (this.#tabs.length > 1) {
719
+ this.#tabs.forEach((tab) => {
720
+ if (tab.getAttribute('data-state') === 'adding') animPromises.push(this.#animateTabAddition(tab));
892
721
  });
893
722
  }
894
- this._updateMeasures();
723
+ this.#updateMeasures();
895
724
  }
896
725
 
897
726
  if (selectedTab) {
898
727
  Promise.all(animPromises).then(async() => {
899
728
  await new Promise(resolve => requestAnimationFrame(resolve));
900
- this._updateMeasures();
901
- this._updateScrollPosition(selectedTab);
729
+ this.#updateMeasures();
730
+ this.#updateScrollPosition(selectedTab);
902
731
  });
903
732
  }
904
733
  }
905
734
 
906
- _isPositionInLeftScrollArea(position) {
735
+ #hideTabsList() {
736
+ // don't animate the tabs list visibility if it's the inital render
737
+ if (reduceMotion || !this.#initialized || !isVisible(this)) {
738
+ this._state = 'hidden';
739
+ } else if (this.shadowRoot) {
740
+ const layout = this.shadowRoot.querySelector('.d2l-tabs-layout');
741
+ const handleTransitionEnd = (e) => {
742
+ if (e.propertyName !== 'max-height') return;
743
+ layout.removeEventListener('transitionend', handleTransitionEnd);
744
+ this._state = 'hidden';
745
+ };
746
+ layout.addEventListener('transitionend', handleTransitionEnd);
747
+ this._state = 'anim';
748
+ }
749
+ }
750
+
751
+ #isPositionInLeftScrollArea(position) {
907
752
  return position > 0 && position < scrollButtonWidth;
908
753
  }
909
754
 
910
- _isPositionInRightScrollArea(position, measures) {
755
+ #isPositionInRightScrollArea(position, measures) {
911
756
  return (position > measures.tabsContainerRect.width - scrollButtonWidth) && (position < measures.tabsContainerRect.width);
912
757
  }
913
758
 
914
- _resetFocusables() {
915
- if (this._defaultSlotBehavior) {
916
- const selectedTab = this._tabInfos.find(ti => ti.selected);
917
- if (selectedTab) this._setFocusableDefaultSlotBehavior(selectedTab);
918
- } else {
919
- const selectedTab = this._tabs.find(ti => ti.selected);
920
- if (selectedTab) this._setFocusable(selectedTab);
921
- }
759
+ #isRTL() {
760
+ return document.documentElement.getAttribute('dir') === 'rtl';
761
+ }
762
+
763
+ #resetFocusables() {
764
+ const selectedTab = this.#tabs.find(ti => ti.selected);
765
+ if (selectedTab) this.#setFocusable(selectedTab);
922
766
  this.requestUpdate();
923
767
  }
924
768
 
925
- _scrollToPosition(translationValue) {
769
+ #scrollToPosition(translationValue) {
926
770
  if (translationValue === this._translationValue) {
927
771
  return Promise.resolve();
928
772
  }
@@ -943,22 +787,44 @@ class Tabs extends LocalizeCoreElement(ArrowKeysMixin(SkeletonMixin(LitElement))
943
787
  });
944
788
  }
945
789
 
946
- _setFocusable(tab) {
947
- const currentFocusable = this._tabs.find(tab => tab.tabIndex === 0);
790
+ #setAriaControls() {
791
+ // debounce so only runs once when tabs/panels slots changing
792
+ if (this.#updateAriaControlsRequested) return;
793
+
794
+ this.#updateAriaControlsRequested = true;
795
+ setTimeout(() => {
796
+ this.#tabs?.forEach((tab) => {
797
+ const panel = this.#getPanel(tab.id);
798
+ if (!panel) {
799
+ console.warn('d2l-tabs: tab without matching panel');
800
+ return;
801
+ }
802
+ tab.setAttribute('aria-controls', `${panel.id}`);
803
+ });
804
+ this.#updateAriaControlsRequested = false;
805
+ }, 0);
806
+ }
807
+
808
+ #setFocusable(tab) {
809
+ const currentFocusable = this.#tabs.find(tab => tab.tabIndex === 0);
948
810
  if (currentFocusable) currentFocusable.tabIndex = -1;
949
811
 
950
812
  tab.tabIndex = 0;
951
813
  }
952
814
 
953
- // remove with GAUD-8299-core-tabs-use-new-structure flag clean up
954
- _setFocusableDefaultSlotBehavior(tabInfo) {
955
- const currentFocusable = this._tabInfos.find(ti => ti.activeFocusable);
956
- if (currentFocusable) currentFocusable.activeFocusable = false;
957
-
958
- tabInfo.activeFocusable = true;
815
+ #showTabsList() {
816
+ // don't animate the tabs list visibility if it's the inital render
817
+ if (reduceMotion || !this.#initialized) {
818
+ this._state = 'shown';
819
+ } else {
820
+ this._state = 'anim';
821
+ requestAnimationFrame(() => {
822
+ this._state = 'shown';
823
+ });
824
+ }
959
825
  }
960
826
 
961
- async _tryExpandTabsContainer(measures) {
827
+ async #tryExpandTabsContainer(measures) {
962
828
 
963
829
  if (!this._scrollCollapsed) return false;
964
830
 
@@ -985,14 +851,14 @@ class Tabs extends LocalizeCoreElement(ArrowKeysMixin(SkeletonMixin(LitElement))
985
851
 
986
852
  await expandedPromise;
987
853
 
988
- this._measures = null;
854
+ this.#measures = null;
989
855
 
990
- await this._updateScrollVisibility(this._getMeasures());
856
+ await this.#updateScrollVisibility(this.#getMeasures());
991
857
  this._maxWidth = null;
992
858
 
993
859
  if (!this._allowScrollNext) {
994
860
  if (!this._allowScrollPrevious) {
995
- this._focusSelected();
861
+ this.#focusSelected();
996
862
  } else {
997
863
  if (this.shadowRoot) this.shadowRoot.querySelector('.d2l-tabs-scroll-previous-container button').focus();
998
864
  }
@@ -1002,14 +868,14 @@ class Tabs extends LocalizeCoreElement(ArrowKeysMixin(SkeletonMixin(LitElement))
1002
868
  return true;
1003
869
  }
1004
870
 
1005
- _updateMeasures() {
871
+ #updateMeasures() {
1006
872
  let totalTabsWidth = 0;
1007
873
  if (!this.shadowRoot) return;
1008
- const tabs = this._defaultSlotBehavior ? [...this.shadowRoot.querySelectorAll('d2l-tab-internal')] : this._tabs;
874
+ const tabs = this.#tabs;
1009
875
 
1010
876
  const tabRects = tabs.map((tab) => {
1011
877
  const tabRect = tab.getBoundingClientRect();
1012
- const offsetLeft = this._defaultSlotBehavior ? tab.offsetLeft : getOffsetLeft(tab, tabRect);
878
+ const offsetLeft = getOffsetLeft(tab, tabRect);
1013
879
 
1014
880
  const measures = {
1015
881
  rect: tabRect,
@@ -1019,7 +885,7 @@ class Tabs extends LocalizeCoreElement(ArrowKeysMixin(SkeletonMixin(LitElement))
1019
885
  return measures;
1020
886
  });
1021
887
 
1022
- this._measures = {
888
+ this.#measures = {
1023
889
  tabsContainerRect: this.shadowRoot.querySelector('.d2l-tabs-container').getBoundingClientRect(),
1024
890
  tabsContainerListRect: this.shadowRoot.querySelector('.d2l-tabs-container-list').getBoundingClientRect(),
1025
891
  tabRects: tabRects,
@@ -1027,20 +893,29 @@ class Tabs extends LocalizeCoreElement(ArrowKeysMixin(SkeletonMixin(LitElement))
1027
893
  };
1028
894
  }
1029
895
 
1030
- _updateScrollPosition(selectedTab) {
1031
- const measures = this._getMeasures();
1032
- const newTranslationValue = this._calculateScrollPosition(selectedTab, measures);
896
+ #updateScrollPosition(selectedTab) {
897
+ const measures = this.#getMeasures();
898
+ const newTranslationValue = this.#calculateScrollPosition(selectedTab, measures);
1033
899
  return this.#updateScrollPositionLogic(measures, newTranslationValue);
1034
900
  }
1035
901
 
1036
- // remove with GAUD-8299-core-tabs-use-new-structure flag clean up
1037
- _updateScrollPositionDefaultSlotBehavior(selectedTabInfo) {
1038
- const measures = this._getMeasures();
1039
- const newTranslationValue = this._calculateScrollPositionDefaultSlotBehavior(selectedTabInfo, measures);
1040
- return this.#updateScrollPositionLogic(measures, newTranslationValue);
902
+ #updateScrollPositionLogic(measures, newTranslationValue) {
903
+ const scrollToPromise = this.#scrollToPosition(newTranslationValue);
904
+ const scrollVisibilityPromise = this.#updateScrollVisibility(measures);
905
+ const p = Promise.all([
906
+ scrollVisibilityPromise,
907
+ scrollToPromise
908
+ ]);
909
+ p.then(() => {
910
+ if (this.#loadingCompleteResolve) {
911
+ this.#loadingCompleteResolve();
912
+ this.#loadingCompleteResolve = undefined;
913
+ }
914
+ });
915
+ return p;
1041
916
  }
1042
917
 
1043
- _updateScrollVisibility(measures) {
918
+ #updateScrollVisibility(measures) {
1044
919
 
1045
920
  const lastTabMeasures = measures.tabRects[measures.tabRects.length - 1];
1046
921
  if (!lastTabMeasures) {
@@ -1060,20 +935,28 @@ class Tabs extends LocalizeCoreElement(ArrowKeysMixin(SkeletonMixin(LitElement))
1060
935
  return this.updateComplete;
1061
936
  }
1062
937
 
1063
- _updateTabListVisibility(tabs) {
1064
- // remove with GAUD-8299-core-tabs-use-new-structure flag clean up
1065
- if (!this.#newTabsPanelStructure) {
1066
- if (this._state === 'shown' && tabs.length < 2) {
1067
- this.#hideTabsList();
1068
- } else if (this._state === 'hidden' && tabs.length > 1) {
1069
- this.#showTabsList();
1070
- } else if (this._state === 'shown' && tabs.length > 1) {
1071
- // check if there are hidden tabs and tab list container should actually be hidden
1072
- this.#handleTabHiddenChange();
938
+ async #updateSelectedTab(selectedTab) {
939
+ await this.updateComplete;
940
+
941
+ selectedTab.selected = true;
942
+ selectedTab.tabIndex = 0;
943
+
944
+ const selectedPanel = this.#getPanel(selectedTab.id);
945
+ if (selectedPanel) selectedPanel._selected = true;
946
+ this.#tabs.forEach((tab) => {
947
+ if (tab.id !== selectedTab.id) {
948
+ if (tab.selected) {
949
+ tab.selected = false;
950
+ const panel = this.#getPanel(tab.id);
951
+ // panel may not exist if it's being removed
952
+ if (panel) panel._selected = false;
953
+ }
954
+ if (tab.tabIndex === 0) tab.tabIndex = -1;
1073
955
  }
1074
- return;
1075
- }
956
+ });
957
+ }
1076
958
 
959
+ #updateTabListVisibility(tabs) {
1077
960
  const visibleCount = tabs.filter(tab => !tab.hidden).length;
1078
961
  if (this._state === 'shown' && visibleCount < 2) {
1079
962
  this.#hideTabsList();
@@ -1085,233 +968,15 @@ class Tabs extends LocalizeCoreElement(ArrowKeysMixin(SkeletonMixin(LitElement))
1085
968
  }
1086
969
  }
1087
970
 
1088
- _updateTabsContainerWidth(selectedTab) {
1089
- const tabs = this._tabs;
971
+ #updateTabsContainerWidth(selectedTab) {
972
+ const tabs = this.#tabs;
1090
973
  if (!this.maxToShow || this.maxToShow <= 0 || this.maxToShow >= tabs.length) return;
1091
974
  if (tabs.indexOf(selectedTab) > this.maxToShow - 1) return;
1092
975
  return this.#updateTabsContainerWidthLogic();
1093
976
  }
1094
977
 
1095
- // remove with GAUD-8299-core-tabs-use-new-structure flag clean up
1096
- _updateTabsContainerWidthDefaultSlotBehavior(selectedTabInfo) {
1097
- if (!this.maxToShow || this.maxToShow <= 0 || this.maxToShow >= this._tabInfos.length) return;
1098
- if (this._tabInfos.indexOf(selectedTabInfo) > this.maxToShow - 1) return;
1099
- return this.#updateTabsContainerWidthLogic();
1100
- }
1101
-
1102
- #calculateScrollPositionLogic(tabsDataStructure, selectedTabIndex, measures) {
1103
- if (!measures.tabRects[selectedTabIndex]) return 0;
1104
-
1105
- const selectedTabMeasures = measures.tabRects[selectedTabIndex];
1106
-
1107
- const isOverflowingLeft = (selectedTabMeasures.offsetLeft + this._translationValue < 0);
1108
- const isOverflowingRight = (selectedTabMeasures.offsetLeft + selectedTabMeasures.rect.width + this._translationValue > measures.tabsContainerRect.width);
1109
-
1110
- const isRTL = this.#isRTL();
1111
-
1112
- let getNewTranslationValue;
1113
- if (!isRTL) {
1114
- getNewTranslationValue = () => {
1115
- if (selectedTabIndex === 0) {
1116
- // position selected tab at beginning
1117
- return 0;
1118
- } else if (selectedTabIndex === (tabsDataStructure.length - 1)) {
1119
- // position selected tab at end
1120
- return -1 * (selectedTabMeasures.offsetLeft - measures.tabsContainerRect.width + selectedTabMeasures.rect.width);
1121
- } else {
1122
- // position selected tab in middle
1123
- return -1 * (selectedTabMeasures.offsetLeft - (measures.tabsContainerRect.width / 2) + (selectedTabMeasures.rect.width / 2));
1124
- }
1125
- };
1126
- } else {
1127
- getNewTranslationValue = () => {
1128
- if (selectedTabIndex === 0) {
1129
- // position selected tab at beginning
1130
- return 0;
1131
- } else if (selectedTabIndex === (tabsDataStructure.length - 1)) {
1132
- // position selected tab at end
1133
- return -1 * selectedTabMeasures.offsetLeft;
1134
- } else {
1135
- // position selected tab in middle
1136
- return (measures.tabsContainerRect.width / 2) - (selectedTabMeasures.offsetLeft + selectedTabMeasures.rect.width / 2) + (selectedTabMeasures.rect.width / 2);
1137
- }
1138
- };
1139
- }
1140
-
1141
- let newTranslationValue = this._translationValue;
1142
- if (isOverflowingLeft || isOverflowingRight) {
1143
- newTranslationValue = getNewTranslationValue();
1144
- }
1145
-
1146
- let expectedPosition;
1147
-
1148
- // make sure the new position will not place selected tab behind left scroll button
1149
- if (!isRTL) {
1150
- expectedPosition = selectedTabMeasures.offsetLeft + newTranslationValue;
1151
- if (newTranslationValue < 0 && this._isPositionInLeftScrollArea(expectedPosition)) {
1152
- newTranslationValue = getNewTranslationValue();
1153
- }
1154
- } else {
1155
- expectedPosition = selectedTabMeasures.offsetLeft + selectedTabMeasures.rect.width + newTranslationValue;
1156
- if (newTranslationValue > 0 && this._isPositionInRightScrollArea(expectedPosition, measures)) {
1157
- newTranslationValue = getNewTranslationValue();
1158
- }
1159
- }
1160
-
1161
- if (!isRTL) {
1162
- // make sure there will not be any empty space between left side of container and first tab
1163
- if (newTranslationValue > 0) newTranslationValue = 0;
1164
- } else {
1165
- // make sure there will not be any empty space between right side of container and first tab
1166
- if (newTranslationValue < 0) newTranslationValue = 0;
1167
- }
1168
-
1169
- // make sure the new position will not place selected tab behind the right scroll button
1170
- if (!isRTL) {
1171
- expectedPosition = selectedTabMeasures.offsetLeft + selectedTabMeasures.rect.width + newTranslationValue;
1172
- if ((selectedTabIndex < tabsDataStructure.length - 1) && this._isPositionInRightScrollArea(expectedPosition, measures)) {
1173
- newTranslationValue = getNewTranslationValue();
1174
- }
1175
- } else {
1176
- expectedPosition = selectedTabMeasures.offsetLeft + newTranslationValue;
1177
- if ((selectedTabIndex < tabsDataStructure.length - 1) && this._isPositionInLeftScrollArea(expectedPosition)) {
1178
- newTranslationValue = getNewTranslationValue();
1179
- }
1180
- }
1181
-
1182
- return newTranslationValue;
1183
- }
1184
-
1185
- #checkTabPanelMatch() {
1186
- // debounce so only runs once when tabs/panels slots changing
1187
- if (this.#checkTabPanelMatchRequested) return;
1188
-
1189
- this.#checkTabPanelMatchRequested = true;
1190
- setTimeout(() => {
1191
- if ((this._tabs && !this.#panels) || (this.#panels && !this._tabs)) {
1192
- console.warn('d2l-tabs: tabs and panels are not in sync');
1193
- } else if (this._tabs.length !== this.#panels.length) {
1194
- console.warn('d2l-tabs: number of tabs and panels does not match');
1195
- }
1196
- this.#checkTabPanelMatchRequested = false;
1197
- }, 0);
1198
- }
1199
-
1200
- #handleTabDeselected(e) {
1201
- const panel = this._getPanel(e.target.id);
1202
- if (panel) {
1203
- if (this.#newTabsPanelStructure) panel._selected = false;
1204
- else panel.selected = false; // remove with GAUD-8299-core-tabs-use-new-structure flag clean up
1205
- }
1206
- }
1207
-
1208
- #handleTabHiddenChange() {
1209
- if (!this._tabs || this._tabs.length <= 1) return;
1210
-
1211
- let visibleTabCount = 0;
1212
- this._tabs.forEach((tab) => {
1213
- if (!tab.hidden) visibleTabCount++;
1214
- });
1215
-
1216
- if (visibleTabCount > 1 && this._state === 'hidden') this.#showTabsList();
1217
- else if (visibleTabCount <= 1 && this._state === 'shown') this.#hideTabsList();
1218
- }
1219
-
1220
- #hideTabsList() {
1221
- // don't animate the tabs list visibility if it's the inital render
1222
- if (reduceMotion || !this._initialized || !isVisible(this)) {
1223
- this._state = 'hidden';
1224
- } else if (this.shadowRoot) {
1225
- const layout = this.shadowRoot.querySelector('.d2l-tabs-layout');
1226
- const handleTransitionEnd = (e) => {
1227
- if (e.propertyName !== 'max-height') return;
1228
- layout.removeEventListener('transitionend', handleTransitionEnd);
1229
- this._state = 'hidden';
1230
- };
1231
- layout.addEventListener('transitionend', handleTransitionEnd);
1232
- this._state = 'anim';
1233
- }
1234
- }
1235
-
1236
- #isRTL() {
1237
- return document.documentElement.getAttribute('dir') === 'rtl';
1238
- }
1239
-
1240
- #setAriaControls() {
1241
- // debounce so only runs once when tabs/panels slots changing
1242
- if (this.#updateAriaControlsRequested) return;
1243
-
1244
- this.#updateAriaControlsRequested = true;
1245
- setTimeout(() => {
1246
- this._tabs?.forEach((tab) => {
1247
- const panel = this._getPanel(tab.id);
1248
- if (!panel) {
1249
- console.warn('d2l-tabs: tab without matching panel');
1250
- return;
1251
- }
1252
- tab.setAttribute('aria-controls', `${panel.id}`);
1253
- });
1254
- this.#updateAriaControlsRequested = false;
1255
- }, 0);
1256
- }
1257
-
1258
- #showTabsList() {
1259
- // don't animate the tabs list visibility if it's the inital render
1260
- if (reduceMotion || !this._initialized) {
1261
- this._state = 'shown';
1262
- } else {
1263
- this._state = 'anim';
1264
- requestAnimationFrame(() => {
1265
- this._state = 'shown';
1266
- });
1267
- }
1268
- }
1269
-
1270
- #updateScrollPositionLogic(measures, newTranslationValue) {
1271
- const scrollToPromise = this._scrollToPosition(newTranslationValue);
1272
- const scrollVisibilityPromise = this._updateScrollVisibility(measures);
1273
- const p = Promise.all([
1274
- scrollVisibilityPromise,
1275
- scrollToPromise
1276
- ]);
1277
- p.then(() => {
1278
- if (this._loadingCompleteResolve) {
1279
- this._loadingCompleteResolve();
1280
- this._loadingCompleteResolve = undefined;
1281
- }
1282
- });
1283
- return p;
1284
- }
1285
-
1286
- async #updateSelectedTab(selectedTab) {
1287
- await this.updateComplete;
1288
-
1289
- selectedTab.selected = true;
1290
- selectedTab.tabIndex = 0;
1291
-
1292
- const selectedPanel = this._getPanel(selectedTab.id);
1293
- if (selectedPanel) {
1294
- if (this.#newTabsPanelStructure) selectedPanel._selected = true;
1295
- else selectedPanel.selected = true; // remove with GAUD-8299-core-tabs-use-new-structure flag clean up
1296
- }
1297
- this._tabs.forEach((tab) => {
1298
- if (tab.id !== selectedTab.id) {
1299
- if (tab.selected) {
1300
- tab.selected = false;
1301
- const panel = this._getPanel(tab.id);
1302
- // panel may not exist if it's being removed
1303
- if (panel) {
1304
- if (this.#newTabsPanelStructure) panel._selected = false;
1305
- else panel.selected = false; // remove with GAUD-8299-core-tabs-use-new-structure flag clean up
1306
- }
1307
- }
1308
- if (tab.tabIndex === 0) tab.tabIndex = -1;
1309
- }
1310
- });
1311
- }
1312
-
1313
978
  #updateTabsContainerWidthLogic() {
1314
- const measures = this._getMeasures();
979
+ const measures = this.#getMeasures();
1315
980
 
1316
981
  let maxWidth = 4; // initial value to allow for padding hack
1317
982
  for (let i = 0; i < this.maxToShow; i++) {
@@ -1326,7 +991,7 @@ class Tabs extends LocalizeCoreElement(ArrowKeysMixin(SkeletonMixin(LitElement))
1326
991
 
1327
992
  this._maxWidth = maxWidth;
1328
993
  this._scrollCollapsed = true;
1329
- this._measures = null;
994
+ this.#measures = null;
1330
995
 
1331
996
  return this.updateComplete;
1332
997
  }