@brightspace-ui/core 3.317.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.
@@ -217,13 +217,13 @@ class Tabs extends LocalizeCoreElement(ArrowKeysMixin(SkeletonMixin(LitElement))
217
217
  this.maxToShow = -1;
218
218
  this._allowScrollNext = false;
219
219
  this._allowScrollPrevious = false;
220
- this._loadingCompleteResolve = undefined;
221
- this._loadingCompletePromise = new Promise(resolve => this._loadingCompleteResolve = resolve);
220
+ this.#loadingCompleteResolve = undefined;
221
+ this.#loadingCompletePromise = new Promise(resolve => this.#loadingCompleteResolve = resolve);
222
222
  this._maxWidth = null;
223
223
  this._scrollCollapsed = false;
224
224
  this._state = 'shown';
225
- this._tabIds = {};
226
- this._tabs = [];
225
+ this.#tabIds = {};
226
+ this.#tabs = [];
227
227
  this._translationValue = 0;
228
228
  }
229
229
 
@@ -231,7 +231,7 @@ class Tabs extends LocalizeCoreElement(ArrowKeysMixin(SkeletonMixin(LitElement))
231
231
  super.connectedCallback();
232
232
 
233
233
  queueMicrotask(() => {
234
- const bgColor = this._getComputedBackgroundColor();
234
+ const bgColor = this.#getComputedBackgroundColor();
235
235
  if (bgColor && bgColor !== 'rgb(255, 255, 255)' && bgColor !== 'rgba(255, 255, 255, 1)') {
236
236
  this.style.setProperty('--d2l-tabs-background-color', bgColor);
237
237
  }
@@ -241,27 +241,27 @@ class Tabs extends LocalizeCoreElement(ArrowKeysMixin(SkeletonMixin(LitElement))
241
241
 
242
242
  disconnectedCallback() {
243
243
  super.disconnectedCallback();
244
- if (this._resizeObserver) this._resizeObserver.disconnect();
244
+ if (this.#resizeObserver) this.#resizeObserver.disconnect();
245
245
  }
246
246
 
247
247
  firstUpdated(changedProperties) {
248
248
  super.firstUpdated(changedProperties);
249
249
 
250
250
  this.arrowKeysFocusablesProvider = async() => {
251
- return this._tabs;
251
+ return this.#tabs;
252
252
  };
253
253
 
254
254
  this.arrowKeysOnBeforeFocus = async(tab) => {
255
- this._setFocusable(tab);
255
+ this.#setFocusable(tab);
256
256
 
257
257
  this.requestUpdate();
258
258
  await this.updateComplete;
259
259
 
260
260
  if (!this._scrollCollapsed) {
261
- return this._updateScrollPosition(tab);
261
+ return this.#updateScrollPosition(tab);
262
262
  } else {
263
- const measures = this._getMeasures();
264
- const newTranslationValue = this._calculateScrollPosition(tab, measures);
263
+ const measures = this.#getMeasures();
264
+ const newTranslationValue = this.#calculateScrollPosition(tab, measures);
265
265
 
266
266
  if (!this.#isRTL()) {
267
267
  if (newTranslationValue >= 0) return;
@@ -269,18 +269,17 @@ class Tabs extends LocalizeCoreElement(ArrowKeysMixin(SkeletonMixin(LitElement))
269
269
  if (newTranslationValue <= 0) return;
270
270
  }
271
271
 
272
- const expanded = await this._tryExpandTabsContainer(measures);
272
+ const expanded = await this.#tryExpandTabsContainer(measures);
273
273
  if (expanded) {
274
274
  return;
275
275
  } else {
276
- return this._updateScrollPosition(tab);
276
+ return this.#updateScrollPosition(tab);
277
277
  }
278
278
  }
279
279
  };
280
280
 
281
- this._handleResize = this._handleResize.bind(this);
282
- this._resizeObserver = new ResizeObserver(this._handleResize);
283
- 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'));
284
283
 
285
284
  }
286
285
 
@@ -313,27 +312,27 @@ class Tabs extends LocalizeCoreElement(ArrowKeysMixin(SkeletonMixin(LitElement))
313
312
  style="${styleMap(tabsContainerStyles)}">
314
313
  <div class="d2l-tabs-scroll-previous-container">
315
314
  <button class="d2l-tabs-scroll-button"
316
- @click="${this._handleScrollPrevious}"
315
+ @click="${this.#handleScrollPrevious}"
317
316
  title="${this.localize('components.tabs.previous')}">
318
317
  <d2l-icon icon="tier1:chevron-left"></d2l-icon>
319
318
  </button>
320
319
  </div>
321
320
  ${this.arrowKeysContainer(html`
322
321
  <div class="d2l-tabs-container-list"
323
- @d2l-tab-content-change="${this._handleTabContentChange}"
322
+ @d2l-tab-content-change="${this.#handleTabContentChange}"
324
323
  @d2l-tab-hidden-change="${this.#handleTabHiddenChange}"
325
- @d2l-tab-selected="${this._handleTabSelected}"
324
+ @d2l-tab-selected="${this.#handleTabSelected}"
326
325
  @d2l-tab-deselected="${this.#handleTabDeselected}"
327
- @focusout="${this._handleFocusOut}"
326
+ @focusout="${this.#handleFocusOut}"
328
327
  aria-label="${ifDefined(this.text)}"
329
328
  role="tablist"
330
329
  style="${styleMap(tabsContainerListStyles)}">
331
- <slot name="tabs" @slotchange="${this._handleTabsSlotChange}"></slot>
330
+ <slot name="tabs" @slotchange="${this.#handleTabsSlotChange}"></slot>
332
331
  </div>
333
332
  `)}
334
333
  <div class="d2l-tabs-scroll-next-container">
335
334
  <button class="d2l-tabs-scroll-button"
336
- @click="${this._handleScrollNext}"
335
+ @click="${this.#handleScrollNext}"
337
336
  title="${this.localize('components.tabs.next')}">
338
337
  <d2l-icon icon="tier1:chevron-right"></d2l-icon>
339
338
  </button>
@@ -342,17 +341,17 @@ class Tabs extends LocalizeCoreElement(ArrowKeysMixin(SkeletonMixin(LitElement))
342
341
  <div class="d2l-tabs-container-ext"><slot name="ext"></slot></div>
343
342
  </div>
344
343
  <div class="${classMap(panelContainerClasses)}">
345
- <slot name="panels" @slotchange="${this._handlePanelsSlotChange}"></slot>
344
+ <slot name="panels" @slotchange="${this.#handlePanelsSlotChange}"></slot>
346
345
  </div>
347
346
  `;
348
347
  }
349
348
 
350
349
  focus() {
351
- return this._focusSelected();
350
+ return this.#focusSelected();
352
351
  }
353
352
 
354
353
  async getLoadingComplete() {
355
- return this._loadingCompletePromise;
354
+ return this.#loadingCompletePromise;
356
355
  }
357
356
 
358
357
  getTabListRect() {
@@ -362,14 +361,28 @@ class Tabs extends LocalizeCoreElement(ArrowKeysMixin(SkeletonMixin(LitElement))
362
361
 
363
362
  hideTab(tab) {
364
363
  tab.setAttribute('data-state', 'removing');
365
- 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();
366
365
  }
367
366
 
368
367
  #checkTabPanelMatchRequested;
368
+ #initialized;
369
+ #loadingCompletePromise;
370
+ #loadingCompleteResolve;
371
+ #measures;
369
372
  #panels;
373
+ #resizeObserver;
374
+ #tabIds;
375
+ #tabs;
370
376
  #updateAriaControlsRequested;
371
377
 
372
- _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) {
373
386
  if (!tab || reduceMotion) {
374
387
  return new Promise((resolve) => {
375
388
  tab.setAttribute('data-state', '');
@@ -390,7 +403,7 @@ class Tabs extends LocalizeCoreElement(ArrowKeysMixin(SkeletonMixin(LitElement))
390
403
  });
391
404
  }
392
405
 
393
- _animateTabRemoval(tab) {
406
+ #animateTabRemoval(tab) {
394
407
  if (!tab || reduceMotion) return Promise.resolve();
395
408
 
396
409
  return new Promise((resolve) => {
@@ -404,22 +417,120 @@ class Tabs extends LocalizeCoreElement(ArrowKeysMixin(SkeletonMixin(LitElement))
404
417
  });
405
418
  }
406
419
 
407
- _calculateScrollPosition(selectedTab, measures) {
408
- const tabs = this._tabs;
420
+ #calculateScrollPosition(selectedTab, measures) {
421
+ const tabs = this.#tabs;
409
422
  const selectedTabIndex = tabs.indexOf(selectedTab);
410
423
  return this.#calculateScrollPositionLogic(tabs, selectedTabIndex, measures);
411
424
  }
412
425
 
413
- async _focusSelected() {
414
- const selectedTab = this._tabs.find(ti => ti.selected);
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
+ }
464
+
465
+ let newTranslationValue = this._translationValue;
466
+ if (isOverflowingLeft || isOverflowingRight) {
467
+ newTranslationValue = getNewTranslationValue();
468
+ }
469
+
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
+ }
484
+
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
+ }
492
+
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);
522
+ }
523
+
524
+ async #focusSelected() {
525
+ const selectedTab = this.#tabs.find(ti => ti.selected);
415
526
  if (!selectedTab) return;
416
527
 
417
- await this._updateScrollPosition(selectedTab);
528
+ await this.#updateScrollPosition(selectedTab);
418
529
 
419
530
  selectedTab.focus();
420
531
  }
421
532
 
422
- _getComputedBackgroundColor() {
533
+ #getComputedBackgroundColor() {
423
534
  let bgColor = null;
424
535
 
425
536
  findComposedAncestor(this, (node) => {
@@ -433,40 +544,33 @@ class Tabs extends LocalizeCoreElement(ArrowKeysMixin(SkeletonMixin(LitElement))
433
544
  return bgColor;
434
545
  }
435
546
 
436
- _getMeasures() {
437
- if (!this._measures) this._updateMeasures();
438
- return this._measures;
547
+ #getMeasures() {
548
+ if (!this.#measures) this.#updateMeasures();
549
+ return this.#measures;
439
550
  }
440
551
 
441
- _getPanel(id) {
552
+ #getPanel(id) {
442
553
  if (!this.#panels) return;
443
554
  return this.#panels.find(panel => panel.labelledBy === id);
444
555
  }
445
556
 
446
- _handleFocusOut(e) {
557
+ #handleFocusOut(e) {
447
558
  if (e.relatedTarget && e.relatedTarget.role === 'tab') return;
448
- this._resetFocusables();
559
+ this.#resetFocusables();
449
560
  }
450
561
 
451
- _handlePanelsSlotChange(e) {
562
+ #handlePanelsSlotChange(e) {
452
563
  this.#panels = e.target.assignedElements({ flatten: true }).filter((node) => node.role === 'tabpanel');
453
564
  this.#checkTabPanelMatch();
454
565
  this.#setAriaControls();
455
566
  }
456
567
 
457
- _handleResize(entries) {
458
- const measures = this._getMeasures();
459
- if (entries.length === 1 && entries[0].contentRect.width === measures.tabsContainerListRect.width) return;
460
- this._updateMeasures();
461
- this._updateScrollVisibility(this._getMeasures());
462
- }
463
-
464
- async _handleScrollNext() {
568
+ async #handleScrollNext() {
465
569
 
466
- const measures = this._getMeasures();
570
+ const measures = this.#getMeasures();
467
571
 
468
- const expanded = await this._tryExpandTabsContainer(measures);
469
- const newMeasures = expanded ? this._getMeasures() : measures;
572
+ const expanded = await this.#tryExpandTabsContainer(measures);
573
+ const newMeasures = expanded ? this.#getMeasures() : measures;
470
574
 
471
575
  let newTranslationValue;
472
576
  const lastTabMeasures = measures.tabRects[measures.tabRects.length - 1];
@@ -496,8 +600,8 @@ class Tabs extends LocalizeCoreElement(ArrowKeysMixin(SkeletonMixin(LitElement))
496
600
 
497
601
  }
498
602
 
499
- await this._scrollToPosition(newTranslationValue);
500
- await this._updateScrollVisibility(newMeasures);
603
+ await this.#scrollToPosition(newTranslationValue);
604
+ await this.#updateScrollVisibility(newMeasures);
501
605
 
502
606
  if (!isOverflowingNext && this.shadowRoot) {
503
607
  this.shadowRoot.querySelector('.d2l-tabs-scroll-previous-container button').focus();
@@ -505,12 +609,12 @@ class Tabs extends LocalizeCoreElement(ArrowKeysMixin(SkeletonMixin(LitElement))
505
609
 
506
610
  }
507
611
 
508
- async _handleScrollPrevious() {
612
+ async #handleScrollPrevious() {
509
613
 
510
- const measures = this._getMeasures();
614
+ const measures = this.#getMeasures();
511
615
 
512
- const expanded = await this._tryExpandTabsContainer(measures);
513
- const newMeasures = expanded ? this._getMeasures() : measures;
616
+ const expanded = await this.#tryExpandTabsContainer(measures);
617
+ const newMeasures = expanded ? this.#getMeasures() : measures;
514
618
 
515
619
  let newTranslationValue;
516
620
  let isOverflowingPrevious;
@@ -529,8 +633,8 @@ class Tabs extends LocalizeCoreElement(ArrowKeysMixin(SkeletonMixin(LitElement))
529
633
 
530
634
  }
531
635
 
532
- await this._scrollToPosition(newTranslationValue);
533
- await this._updateScrollVisibility(newMeasures);
636
+ await this.#scrollToPosition(newTranslationValue);
637
+ await this.#updateScrollVisibility(newMeasures);
534
638
 
535
639
  if (!isOverflowingPrevious && this.shadowRoot) {
536
640
  this.shadowRoot.querySelector('.d2l-tabs-scroll-next-container button').focus();
@@ -538,33 +642,50 @@ class Tabs extends LocalizeCoreElement(ArrowKeysMixin(SkeletonMixin(LitElement))
538
642
 
539
643
  }
540
644
 
541
- async _handleTabContentChange() {
542
- this._updateMeasures();
543
- await this._updateScrollVisibility(this._getMeasures());
645
+ async #handleTabContentChange() {
646
+ this.#updateMeasures();
647
+ await this.#updateScrollVisibility(this.#getMeasures());
648
+ }
649
+
650
+ #handleTabDeselected(e) {
651
+ const panel = this.#getPanel(e.target.id);
652
+ if (panel) panel._selected = false;
544
653
  }
545
654
 
546
- async _handleTabSelected(e) {
655
+ #handleTabHiddenChange() {
656
+ if (!this.#tabs || this.#tabs.length <= 1) return;
657
+
658
+ let visibleTabCount = 0;
659
+ this.#tabs.forEach((tab) => {
660
+ if (!tab.hidden) visibleTabCount++;
661
+ });
662
+
663
+ if (visibleTabCount > 1 && this._state === 'hidden') this.#showTabsList();
664
+ else if (visibleTabCount <= 1 && this._state === 'shown') this.#hideTabsList();
665
+ }
666
+
667
+ async #handleTabSelected(e) {
547
668
  const selectedTab = e.target;
548
669
  this.#updateSelectedTab(selectedTab);
549
670
  await this.updateComplete;
550
- this._updateScrollPosition(selectedTab);
671
+ this.#updateScrollPosition(selectedTab);
551
672
  }
552
673
 
553
- async _handleTabsSlotChange(e) {
554
- 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');
555
676
 
556
677
  // handle case where there are less than two tabs initially
557
- this._updateTabListVisibility(this._tabs);
678
+ this.#updateTabListVisibility(this.#tabs);
558
679
 
559
- if (!this._initialized && this._tabs.length === 0) return;
680
+ if (!this.#initialized && this.#tabs.length === 0) return;
560
681
 
561
682
  let selectedTab = null;
562
683
  const newTabIds = {};
563
- this._tabs?.forEach((tab) => {
564
- const isNew = this._initialized && !this._tabIds[tab.id];
565
- 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) {
566
687
  // if it's a new tab, update state to animate addition
567
- this._tabIds[tab.id] = true;
688
+ this.#tabIds[tab.id] = true;
568
689
  tab.setAttribute('data-state', 'adding');
569
690
  }
570
691
  if (tab.selected && tab.getAttribute('data-state') !== 'removing') {
@@ -574,10 +695,10 @@ class Tabs extends LocalizeCoreElement(ArrowKeysMixin(SkeletonMixin(LitElement))
574
695
  newTabIds[tab.id] = true;
575
696
  });
576
697
 
577
- this._tabIds = newTabIds;
698
+ this.#tabIds = newTabIds;
578
699
 
579
700
  if (!selectedTab) {
580
- selectedTab = this._tabs.find((tab) => tab.getAttribute('data-state') !== 'removing');
701
+ selectedTab = this.#tabs.find((tab) => tab.getAttribute('data-state') !== 'removing');
581
702
  if (selectedTab) selectedTab.selected = true;
582
703
  }
583
704
  if (selectedTab) {
@@ -590,42 +711,62 @@ class Tabs extends LocalizeCoreElement(ArrowKeysMixin(SkeletonMixin(LitElement))
590
711
 
591
712
  const animPromises = [];
592
713
 
593
- if (!this._initialized && this._tabs.length > 0) {
594
- this._initialized = true;
595
- await this._updateTabsContainerWidth(selectedTab);
714
+ if (!this.#initialized && this.#tabs.length > 0) {
715
+ this.#initialized = true;
716
+ await this.#updateTabsContainerWidth(selectedTab);
596
717
  } else {
597
- if (this._tabs.length > 1) {
598
- this._tabs.forEach((tab) => {
599
- 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));
600
721
  });
601
722
  }
602
- this._updateMeasures();
723
+ this.#updateMeasures();
603
724
  }
604
725
 
605
726
  if (selectedTab) {
606
727
  Promise.all(animPromises).then(async() => {
607
728
  await new Promise(resolve => requestAnimationFrame(resolve));
608
- this._updateMeasures();
609
- this._updateScrollPosition(selectedTab);
729
+ this.#updateMeasures();
730
+ this.#updateScrollPosition(selectedTab);
610
731
  });
611
732
  }
612
733
  }
613
734
 
614
- _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) {
615
752
  return position > 0 && position < scrollButtonWidth;
616
753
  }
617
754
 
618
- _isPositionInRightScrollArea(position, measures) {
755
+ #isPositionInRightScrollArea(position, measures) {
619
756
  return (position > measures.tabsContainerRect.width - scrollButtonWidth) && (position < measures.tabsContainerRect.width);
620
757
  }
621
758
 
622
- _resetFocusables() {
623
- const selectedTab = this._tabs.find(ti => ti.selected);
624
- if (selectedTab) this._setFocusable(selectedTab);
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);
625
766
  this.requestUpdate();
626
767
  }
627
768
 
628
- _scrollToPosition(translationValue) {
769
+ #scrollToPosition(translationValue) {
629
770
  if (translationValue === this._translationValue) {
630
771
  return Promise.resolve();
631
772
  }
@@ -646,14 +787,44 @@ class Tabs extends LocalizeCoreElement(ArrowKeysMixin(SkeletonMixin(LitElement))
646
787
  });
647
788
  }
648
789
 
649
- _setFocusable(tab) {
650
- 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);
651
810
  if (currentFocusable) currentFocusable.tabIndex = -1;
652
811
 
653
812
  tab.tabIndex = 0;
654
813
  }
655
814
 
656
- async _tryExpandTabsContainer(measures) {
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
+ }
825
+ }
826
+
827
+ async #tryExpandTabsContainer(measures) {
657
828
 
658
829
  if (!this._scrollCollapsed) return false;
659
830
 
@@ -680,14 +851,14 @@ class Tabs extends LocalizeCoreElement(ArrowKeysMixin(SkeletonMixin(LitElement))
680
851
 
681
852
  await expandedPromise;
682
853
 
683
- this._measures = null;
854
+ this.#measures = null;
684
855
 
685
- await this._updateScrollVisibility(this._getMeasures());
856
+ await this.#updateScrollVisibility(this.#getMeasures());
686
857
  this._maxWidth = null;
687
858
 
688
859
  if (!this._allowScrollNext) {
689
860
  if (!this._allowScrollPrevious) {
690
- this._focusSelected();
861
+ this.#focusSelected();
691
862
  } else {
692
863
  if (this.shadowRoot) this.shadowRoot.querySelector('.d2l-tabs-scroll-previous-container button').focus();
693
864
  }
@@ -697,10 +868,10 @@ class Tabs extends LocalizeCoreElement(ArrowKeysMixin(SkeletonMixin(LitElement))
697
868
  return true;
698
869
  }
699
870
 
700
- _updateMeasures() {
871
+ #updateMeasures() {
701
872
  let totalTabsWidth = 0;
702
873
  if (!this.shadowRoot) return;
703
- const tabs = this._tabs;
874
+ const tabs = this.#tabs;
704
875
 
705
876
  const tabRects = tabs.map((tab) => {
706
877
  const tabRect = tab.getBoundingClientRect();
@@ -714,7 +885,7 @@ class Tabs extends LocalizeCoreElement(ArrowKeysMixin(SkeletonMixin(LitElement))
714
885
  return measures;
715
886
  });
716
887
 
717
- this._measures = {
888
+ this.#measures = {
718
889
  tabsContainerRect: this.shadowRoot.querySelector('.d2l-tabs-container').getBoundingClientRect(),
719
890
  tabsContainerListRect: this.shadowRoot.querySelector('.d2l-tabs-container-list').getBoundingClientRect(),
720
891
  tabRects: tabRects,
@@ -722,13 +893,29 @@ class Tabs extends LocalizeCoreElement(ArrowKeysMixin(SkeletonMixin(LitElement))
722
893
  };
723
894
  }
724
895
 
725
- _updateScrollPosition(selectedTab) {
726
- const measures = this._getMeasures();
727
- const newTranslationValue = this._calculateScrollPosition(selectedTab, measures);
896
+ #updateScrollPosition(selectedTab) {
897
+ const measures = this.#getMeasures();
898
+ const newTranslationValue = this.#calculateScrollPosition(selectedTab, measures);
728
899
  return this.#updateScrollPositionLogic(measures, newTranslationValue);
729
900
  }
730
901
 
731
- _updateScrollVisibility(measures) {
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;
916
+ }
917
+
918
+ #updateScrollVisibility(measures) {
732
919
 
733
920
  const lastTabMeasures = measures.tabRects[measures.tabRects.length - 1];
734
921
  if (!lastTabMeasures) {
@@ -748,219 +935,19 @@ class Tabs extends LocalizeCoreElement(ArrowKeysMixin(SkeletonMixin(LitElement))
748
935
  return this.updateComplete;
749
936
  }
750
937
 
751
- _updateTabListVisibility(tabs) {
752
- const visibleCount = tabs.filter(tab => !tab.hidden).length;
753
- if (this._state === 'shown' && visibleCount < 2) {
754
- this.#hideTabsList();
755
- } else if (this._state === 'hidden' && visibleCount > 1) {
756
- this.#showTabsList();
757
- } else if (this._state === 'shown' && visibleCount > 1) {
758
- // check if there are hidden tabs and tab list container should actually be hidden
759
- this.#handleTabHiddenChange();
760
- }
761
- }
762
-
763
- _updateTabsContainerWidth(selectedTab) {
764
- const tabs = this._tabs;
765
- if (!this.maxToShow || this.maxToShow <= 0 || this.maxToShow >= tabs.length) return;
766
- if (tabs.indexOf(selectedTab) > this.maxToShow - 1) return;
767
- return this.#updateTabsContainerWidthLogic();
768
- }
769
-
770
- #calculateScrollPositionLogic(tabsDataStructure, selectedTabIndex, measures) {
771
- if (!measures.tabRects[selectedTabIndex]) return 0;
772
-
773
- const selectedTabMeasures = measures.tabRects[selectedTabIndex];
774
-
775
- const isOverflowingLeft = (selectedTabMeasures.offsetLeft + this._translationValue < 0);
776
- const isOverflowingRight = (selectedTabMeasures.offsetLeft + selectedTabMeasures.rect.width + this._translationValue > measures.tabsContainerRect.width);
777
-
778
- const isRTL = this.#isRTL();
779
-
780
- let getNewTranslationValue;
781
- if (!isRTL) {
782
- getNewTranslationValue = () => {
783
- if (selectedTabIndex === 0) {
784
- // position selected tab at beginning
785
- return 0;
786
- } else if (selectedTabIndex === (tabsDataStructure.length - 1)) {
787
- // position selected tab at end
788
- return -1 * (selectedTabMeasures.offsetLeft - measures.tabsContainerRect.width + selectedTabMeasures.rect.width);
789
- } else {
790
- // position selected tab in middle
791
- return -1 * (selectedTabMeasures.offsetLeft - (measures.tabsContainerRect.width / 2) + (selectedTabMeasures.rect.width / 2));
792
- }
793
- };
794
- } else {
795
- getNewTranslationValue = () => {
796
- if (selectedTabIndex === 0) {
797
- // position selected tab at beginning
798
- return 0;
799
- } else if (selectedTabIndex === (tabsDataStructure.length - 1)) {
800
- // position selected tab at end
801
- return -1 * selectedTabMeasures.offsetLeft;
802
- } else {
803
- // position selected tab in middle
804
- return (measures.tabsContainerRect.width / 2) - (selectedTabMeasures.offsetLeft + selectedTabMeasures.rect.width / 2) + (selectedTabMeasures.rect.width / 2);
805
- }
806
- };
807
- }
808
-
809
- let newTranslationValue = this._translationValue;
810
- if (isOverflowingLeft || isOverflowingRight) {
811
- newTranslationValue = getNewTranslationValue();
812
- }
813
-
814
- let expectedPosition;
815
-
816
- // make sure the new position will not place selected tab behind left scroll button
817
- if (!isRTL) {
818
- expectedPosition = selectedTabMeasures.offsetLeft + newTranslationValue;
819
- if (newTranslationValue < 0 && this._isPositionInLeftScrollArea(expectedPosition)) {
820
- newTranslationValue = getNewTranslationValue();
821
- }
822
- } else {
823
- expectedPosition = selectedTabMeasures.offsetLeft + selectedTabMeasures.rect.width + newTranslationValue;
824
- if (newTranslationValue > 0 && this._isPositionInRightScrollArea(expectedPosition, measures)) {
825
- newTranslationValue = getNewTranslationValue();
826
- }
827
- }
828
-
829
- if (!isRTL) {
830
- // make sure there will not be any empty space between left side of container and first tab
831
- if (newTranslationValue > 0) newTranslationValue = 0;
832
- } else {
833
- // make sure there will not be any empty space between right side of container and first tab
834
- if (newTranslationValue < 0) newTranslationValue = 0;
835
- }
836
-
837
- // make sure the new position will not place selected tab behind the right scroll button
838
- if (!isRTL) {
839
- expectedPosition = selectedTabMeasures.offsetLeft + selectedTabMeasures.rect.width + newTranslationValue;
840
- if ((selectedTabIndex < tabsDataStructure.length - 1) && this._isPositionInRightScrollArea(expectedPosition, measures)) {
841
- newTranslationValue = getNewTranslationValue();
842
- }
843
- } else {
844
- expectedPosition = selectedTabMeasures.offsetLeft + newTranslationValue;
845
- if ((selectedTabIndex < tabsDataStructure.length - 1) && this._isPositionInLeftScrollArea(expectedPosition)) {
846
- newTranslationValue = getNewTranslationValue();
847
- }
848
- }
849
-
850
- return newTranslationValue;
851
- }
852
-
853
- #checkTabPanelMatch() {
854
- // debounce so only runs once when tabs/panels slots changing
855
- if (this.#checkTabPanelMatchRequested) return;
856
-
857
- this.#checkTabPanelMatchRequested = true;
858
- setTimeout(() => {
859
- if ((this._tabs && !this.#panels) || (this.#panels && !this._tabs)) {
860
- console.warn('d2l-tabs: tabs and panels are not in sync');
861
- } else if (this._tabs.length !== this.#panels.length) {
862
- console.warn('d2l-tabs: number of tabs and panels does not match');
863
- }
864
- this.#checkTabPanelMatchRequested = false;
865
- }, 0);
866
- }
867
-
868
- #handleTabDeselected(e) {
869
- const panel = this._getPanel(e.target.id);
870
- if (panel) panel._selected = false;
871
- }
872
-
873
- #handleTabHiddenChange() {
874
- if (!this._tabs || this._tabs.length <= 1) return;
875
-
876
- let visibleTabCount = 0;
877
- this._tabs.forEach((tab) => {
878
- if (!tab.hidden) visibleTabCount++;
879
- });
880
-
881
- if (visibleTabCount > 1 && this._state === 'hidden') this.#showTabsList();
882
- else if (visibleTabCount <= 1 && this._state === 'shown') this.#hideTabsList();
883
- }
884
-
885
- #hideTabsList() {
886
- // don't animate the tabs list visibility if it's the inital render
887
- if (reduceMotion || !this._initialized || !isVisible(this)) {
888
- this._state = 'hidden';
889
- } else if (this.shadowRoot) {
890
- const layout = this.shadowRoot.querySelector('.d2l-tabs-layout');
891
- const handleTransitionEnd = (e) => {
892
- if (e.propertyName !== 'max-height') return;
893
- layout.removeEventListener('transitionend', handleTransitionEnd);
894
- this._state = 'hidden';
895
- };
896
- layout.addEventListener('transitionend', handleTransitionEnd);
897
- this._state = 'anim';
898
- }
899
- }
900
-
901
- #isRTL() {
902
- return document.documentElement.getAttribute('dir') === 'rtl';
903
- }
904
-
905
- #setAriaControls() {
906
- // debounce so only runs once when tabs/panels slots changing
907
- if (this.#updateAriaControlsRequested) return;
908
-
909
- this.#updateAriaControlsRequested = true;
910
- setTimeout(() => {
911
- this._tabs?.forEach((tab) => {
912
- const panel = this._getPanel(tab.id);
913
- if (!panel) {
914
- console.warn('d2l-tabs: tab without matching panel');
915
- return;
916
- }
917
- tab.setAttribute('aria-controls', `${panel.id}`);
918
- });
919
- this.#updateAriaControlsRequested = false;
920
- }, 0);
921
- }
922
-
923
- #showTabsList() {
924
- // don't animate the tabs list visibility if it's the inital render
925
- if (reduceMotion || !this._initialized) {
926
- this._state = 'shown';
927
- } else {
928
- this._state = 'anim';
929
- requestAnimationFrame(() => {
930
- this._state = 'shown';
931
- });
932
- }
933
- }
934
-
935
- #updateScrollPositionLogic(measures, newTranslationValue) {
936
- const scrollToPromise = this._scrollToPosition(newTranslationValue);
937
- const scrollVisibilityPromise = this._updateScrollVisibility(measures);
938
- const p = Promise.all([
939
- scrollVisibilityPromise,
940
- scrollToPromise
941
- ]);
942
- p.then(() => {
943
- if (this._loadingCompleteResolve) {
944
- this._loadingCompleteResolve();
945
- this._loadingCompleteResolve = undefined;
946
- }
947
- });
948
- return p;
949
- }
950
-
951
938
  async #updateSelectedTab(selectedTab) {
952
939
  await this.updateComplete;
953
940
 
954
941
  selectedTab.selected = true;
955
942
  selectedTab.tabIndex = 0;
956
943
 
957
- const selectedPanel = this._getPanel(selectedTab.id);
944
+ const selectedPanel = this.#getPanel(selectedTab.id);
958
945
  if (selectedPanel) selectedPanel._selected = true;
959
- this._tabs.forEach((tab) => {
946
+ this.#tabs.forEach((tab) => {
960
947
  if (tab.id !== selectedTab.id) {
961
948
  if (tab.selected) {
962
949
  tab.selected = false;
963
- const panel = this._getPanel(tab.id);
950
+ const panel = this.#getPanel(tab.id);
964
951
  // panel may not exist if it's being removed
965
952
  if (panel) panel._selected = false;
966
953
  }
@@ -969,8 +956,27 @@ class Tabs extends LocalizeCoreElement(ArrowKeysMixin(SkeletonMixin(LitElement))
969
956
  });
970
957
  }
971
958
 
959
+ #updateTabListVisibility(tabs) {
960
+ const visibleCount = tabs.filter(tab => !tab.hidden).length;
961
+ if (this._state === 'shown' && visibleCount < 2) {
962
+ this.#hideTabsList();
963
+ } else if (this._state === 'hidden' && visibleCount > 1) {
964
+ this.#showTabsList();
965
+ } else if (this._state === 'shown' && visibleCount > 1) {
966
+ // check if there are hidden tabs and tab list container should actually be hidden
967
+ this.#handleTabHiddenChange();
968
+ }
969
+ }
970
+
971
+ #updateTabsContainerWidth(selectedTab) {
972
+ const tabs = this.#tabs;
973
+ if (!this.maxToShow || this.maxToShow <= 0 || this.maxToShow >= tabs.length) return;
974
+ if (tabs.indexOf(selectedTab) > this.maxToShow - 1) return;
975
+ return this.#updateTabsContainerWidthLogic();
976
+ }
977
+
972
978
  #updateTabsContainerWidthLogic() {
973
- const measures = this._getMeasures();
979
+ const measures = this.#getMeasures();
974
980
 
975
981
  let maxWidth = 4; // initial value to allow for padding hack
976
982
  for (let i = 0; i < this.maxToShow; i++) {
@@ -985,7 +991,7 @@ class Tabs extends LocalizeCoreElement(ArrowKeysMixin(SkeletonMixin(LitElement))
985
991
 
986
992
  this._maxWidth = maxWidth;
987
993
  this._scrollCollapsed = true;
988
- this._measures = null;
994
+ this.#measures = null;
989
995
 
990
996
  return this.updateComplete;
991
997
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brightspace-ui/core",
3
- "version": "3.317.0",
3
+ "version": "3.317.1",
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",