@brightspace-ui/core 3.94.0 → 3.94.2

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.
@@ -44,9 +44,27 @@
44
44
  <d2l-tab id="all" text="All" slot="tabs"></d2l-tab>
45
45
  <d2l-tab id="biology" text="Biology" slot="tabs" selected></d2l-tab>
46
46
  <d2l-tab id="chemistry" text="Chemistry" slot="tabs"></d2l-tab>
47
+ <d2l-tab id="physics" text="Physics" slot="tabs"></d2l-tab>
48
+ <d2l-tab id="math" text="Math" slot="tabs"></d2l-tab>
49
+ <d2l-tab id="earth-sciences" text="Earth Sciences" slot="tabs"></d2l-tab>
47
50
  <d2l-tab-panel labelled-by="all" slot="panels" id="all-panel">Tab content for All</d2l-tab-panel>
48
51
  <d2l-tab-panel labelled-by="biology" slot="panels" id="biology-panel">Tab content for Biology</d2l-tab-panel>
49
52
  <d2l-tab-panel labelled-by="chemistry" slot="panels">Tab content for Chemistry</d2l-tab-panel>
53
+ <d2l-tab-panel labelled-by="physics" slot="panels">Tab content for Physics</d2l-tab-panel>
54
+ <d2l-tab-panel labelled-by="math" slot="panels">Tab content for Math</d2l-tab-panel>
55
+ <d2l-tab-panel labelled-by="earth-sciences" slot="panels">Tab content for Earth Sciences</d2l-tab-panel>
56
+ <d2l-dropdown-button-subtle slot="ext" text="Explore Topics">
57
+ <d2l-dropdown-menu>
58
+ <d2l-menu label="Astronomy">
59
+ <d2l-menu-item text="Introduction"></d2l-menu-item>
60
+ <d2l-menu-item text="Searching for the Heavens "></d2l-menu-item>
61
+ <d2l-menu-item text="The Solar System"></d2l-menu-item>
62
+ <d2l-menu-item text="Stars &amp; Galaxies"></d2l-menu-item>
63
+ <d2l-menu-item text="The Night Sky"></d2l-menu-item>
64
+ <d2l-menu-item text="The Universe"></d2l-menu-item>
65
+ </d2l-menu>
66
+ </d2l-dropdown-menu>
67
+ </d2l-dropdown-button-subtle>
50
68
  </d2l-tabs>
51
69
  </template>
52
70
  </d2l-demo-snippet>
@@ -3,7 +3,7 @@ import '../icons/icon.js';
3
3
  import '../../helpers/queueMicrotask.js';
4
4
  import './tab-internal.js';
5
5
  import { css, html, LitElement, unsafeCSS } from 'lit';
6
- import { cssEscape, findComposedAncestor } from '../../helpers/dom.js';
6
+ import { cssEscape, findComposedAncestor, getOffsetParent } from '../../helpers/dom.js';
7
7
  import { ArrowKeysMixin } from '../../mixins/arrow-keys/arrow-keys-mixin.js';
8
8
  import { bodyCompactStyles } from '../typography/styles.js';
9
9
  import { classMap } from 'lit/directives/class-map.js';
@@ -19,6 +19,11 @@ const reduceMotion = matchMedia('(prefers-reduced-motion: reduce)').matches;
19
19
 
20
20
  const scrollButtonWidth = 56;
21
21
 
22
+ function getOffsetLeft(tab, tabRect) {
23
+ const offsetParent = getOffsetParent(tab);
24
+ return Math.round(tabRect.left - offsetParent.getBoundingClientRect().left);
25
+ }
26
+
22
27
  /**
23
28
  * A component for tabbed content. It supports the "d2l-tab-panel" component for the content, renders tabs responsively, and provides virtual scrolling for large tab lists.
24
29
  * @slot - Contains the tab panels (e.g., "d2l-tab-panel" components)
@@ -94,6 +99,7 @@ class Tabs extends LocalizeCoreElement(ArrowKeysMixin(SkeletonMixin(LitElement))
94
99
  }
95
100
  .d2l-tabs-container-list {
96
101
  display: flex;
102
+ position: relative;
97
103
  -webkit-transition: transform 200ms ease-out;
98
104
  transition: transform 200ms ease-out;
99
105
  white-space: nowrap;
@@ -247,10 +253,10 @@ class Tabs extends LocalizeCoreElement(ArrowKeysMixin(SkeletonMixin(LitElement))
247
253
  await this.updateComplete;
248
254
 
249
255
  if (!this._scrollCollapsed) {
250
- return this._updateScrollPosition(tabInfo);
256
+ return this._updateScrollPositionDefaultSlotBehavior(tabInfo);
251
257
  } else {
252
258
  const measures = this._getMeasures();
253
- const newTranslationValue = this._calculateScrollPosition(tabInfo, measures);
259
+ const newTranslationValue = this._calculateScrollPositionDefaultSlotBehavior(tabInfo, measures);
254
260
 
255
261
  if (!this.#isRTL()) {
256
262
  if (newTranslationValue >= 0) return;
@@ -262,7 +268,7 @@ class Tabs extends LocalizeCoreElement(ArrowKeysMixin(SkeletonMixin(LitElement))
262
268
  if (expanded) {
263
269
  return;
264
270
  } else {
265
- return this._updateScrollPosition(tabInfo);
271
+ return this._updateScrollPositionDefaultSlotBehavior(tabInfo);
266
272
  }
267
273
  }
268
274
  };
@@ -350,7 +356,7 @@ class Tabs extends LocalizeCoreElement(ArrowKeysMixin(SkeletonMixin(LitElement))
350
356
  }
351
357
 
352
358
  async getLoadingComplete() {
353
- return this._defaultSlotBehavior ? this._loadingCompletePromise : true;
359
+ return this._loadingCompletePromise;
354
360
  }
355
361
 
356
362
  getTabListRect() {
@@ -358,6 +364,10 @@ class Tabs extends LocalizeCoreElement(ArrowKeysMixin(SkeletonMixin(LitElement))
358
364
  return this.shadowRoot.querySelector('.d2l-tabs-container-list').getBoundingClientRect();
359
365
  }
360
366
 
367
+ #checkTabPanelMatchRequested;
368
+ #panels;
369
+ #updateAriaControlsRequested;
370
+
361
371
  _animateTabAddition(tabInfo) {
362
372
  const tab = this.shadowRoot
363
373
  && this.shadowRoot.querySelector(`d2l-tab-internal[controls-panel="${cssEscape(tabInfo.id)}"]`);
@@ -388,91 +398,16 @@ class Tabs extends LocalizeCoreElement(ArrowKeysMixin(SkeletonMixin(LitElement))
388
398
  });
389
399
  }
390
400
 
391
- _calculateScrollPosition(selectedTabInfo, measures) {
401
+ _calculateScrollPosition(selectedTab, measures) {
402
+ const tabs = this._tabs;
403
+ const selectedTabIndex = tabs.indexOf(selectedTab);
404
+ return this.#calculateScrollPositionLogic(tabs, selectedTabIndex, measures);
405
+ }
392
406
 
407
+ // remove after d2l-tab/d2l-tab-panel backport
408
+ _calculateScrollPositionDefaultSlotBehavior(selectedTabInfo, measures) {
393
409
  const selectedTabIndex = this._tabInfos.indexOf(selectedTabInfo);
394
-
395
- if (!measures.tabRects[selectedTabIndex]) return 0;
396
-
397
- const selectedTabMeasures = measures.tabRects[selectedTabIndex];
398
-
399
- const isOverflowingLeft = (selectedTabMeasures.offsetLeft + this._translationValue < 0);
400
- const isOverflowingRight = (selectedTabMeasures.offsetLeft + selectedTabMeasures.rect.width + this._translationValue > measures.tabsContainerRect.width);
401
-
402
- const isRTL = this.#isRTL();
403
-
404
- let getNewTranslationValue;
405
- if (!isRTL) {
406
- getNewTranslationValue = () => {
407
- if (selectedTabIndex === 0) {
408
- // position selected tab at beginning
409
- return 0;
410
- } else if (selectedTabIndex === (this._tabInfos.length - 1)) {
411
- // position selected tab at end
412
- return -1 * (selectedTabMeasures.offsetLeft - measures.tabsContainerRect.width + selectedTabMeasures.rect.width);
413
- } else {
414
- // position selected tab in middle
415
- return -1 * (selectedTabMeasures.offsetLeft - (measures.tabsContainerRect.width / 2) + (selectedTabMeasures.rect.width / 2));
416
- }
417
- };
418
- } else {
419
- getNewTranslationValue = () => {
420
- if (selectedTabIndex === 0) {
421
- // position selected tab at beginning
422
- return 0;
423
- } else if (selectedTabIndex === (this._tabInfos.length - 1)) {
424
- // position selected tab at end
425
- return -1 * selectedTabMeasures.offsetLeft;
426
- } else {
427
- // position selected tab in middle
428
- return (measures.tabsContainerRect.width / 2) - (selectedTabMeasures.offsetLeft + selectedTabMeasures.rect.width / 2) + (selectedTabMeasures.rect.width / 2);
429
- }
430
- };
431
- }
432
-
433
- let newTranslationValue = this._translationValue;
434
- if (isOverflowingLeft || isOverflowingRight) {
435
- newTranslationValue = getNewTranslationValue();
436
- }
437
-
438
- let expectedPosition;
439
-
440
- // make sure the new position will not place selected tab behind left scroll button
441
- if (!isRTL) {
442
- expectedPosition = selectedTabMeasures.offsetLeft + newTranslationValue;
443
- if (newTranslationValue < 0 && this._isPositionInLeftScrollArea(expectedPosition)) {
444
- newTranslationValue = getNewTranslationValue();
445
- }
446
- } else {
447
- expectedPosition = selectedTabMeasures.offsetLeft + selectedTabMeasures.rect.width + newTranslationValue;
448
- if (newTranslationValue > 0 && this._isPositionInRightScrollArea(expectedPosition, measures)) {
449
- newTranslationValue = getNewTranslationValue();
450
- }
451
- }
452
-
453
- if (!isRTL) {
454
- // make sure there will not be any empty space between left side of container and first tab
455
- if (newTranslationValue > 0) newTranslationValue = 0;
456
- } else {
457
- // make sure there will not be any empty space between right side of container and first tab
458
- if (newTranslationValue < 0) newTranslationValue = 0;
459
- }
460
-
461
- // make sure the new position will not place selected tab behind the right scroll button
462
- if (!isRTL) {
463
- expectedPosition = selectedTabMeasures.offsetLeft + selectedTabMeasures.rect.width + newTranslationValue;
464
- if ((selectedTabIndex < this._tabInfos.length - 1) && this._isPositionInRightScrollArea(expectedPosition, measures)) {
465
- newTranslationValue = getNewTranslationValue();
466
- }
467
- } else {
468
- expectedPosition = selectedTabMeasures.offsetLeft + newTranslationValue;
469
- if ((selectedTabIndex < this._tabInfos.length - 1) && this._isPositionInLeftScrollArea(expectedPosition)) {
470
- newTranslationValue = getNewTranslationValue();
471
- }
472
- }
473
-
474
- return newTranslationValue;
475
-
410
+ return this.#calculateScrollPositionLogic(this._tabInfos, selectedTabIndex, measures);
476
411
  }
477
412
 
478
413
  async _focusSelected() {
@@ -480,7 +415,7 @@ class Tabs extends LocalizeCoreElement(ArrowKeysMixin(SkeletonMixin(LitElement))
480
415
  if (!selectedTab) return;
481
416
 
482
417
  const selectedTabInfo = this._getTabInfo(selectedTab.controlsPanel);
483
- await this._updateScrollPosition(selectedTabInfo);
418
+ await this._updateScrollPositionDefaultSlotBehavior(selectedTabInfo);
484
419
 
485
420
  selectedTab.focus();
486
421
  }
@@ -507,10 +442,8 @@ class Tabs extends LocalizeCoreElement(ArrowKeysMixin(SkeletonMixin(LitElement))
507
442
  _getPanel(id) {
508
443
  if (this._defaultSlotBehavior) return this._getPanelDefaultSlotBehavior(id);
509
444
 
510
- if (!this.shadowRoot) return;
511
- const slot = this.shadowRoot.querySelector('slot[name="panels"]');
512
- const panels = this._getPanels(slot);
513
- return panels.find(panel => panel.labelledBy === id);
445
+ if (!this.#panels) return;
446
+ return this.#panels.find(panel => panel.labelledBy === id);
514
447
  }
515
448
 
516
449
  // remove after d2l-tab/d2l-tab-panel backport
@@ -518,7 +451,7 @@ class Tabs extends LocalizeCoreElement(ArrowKeysMixin(SkeletonMixin(LitElement))
518
451
  if (!this.shadowRoot) return;
519
452
  // use simple selector for slot (Edge)
520
453
  const slot = this.shadowRoot.querySelector('.d2l-panels-container').querySelector('slot');
521
- const panels = this._getPanels(slot);
454
+ const panels = this._getPanelsDefaultSlotBehavior(slot);
522
455
  for (let i = 0; i < panels.length; i++) {
523
456
  if (panels[i].nodeType === Node.ELEMENT_NODE && panels[i].role === 'tabpanel' && panels[i].id === id) {
524
457
  return panels[i];
@@ -526,7 +459,8 @@ class Tabs extends LocalizeCoreElement(ArrowKeysMixin(SkeletonMixin(LitElement))
526
459
  }
527
460
  }
528
461
 
529
- _getPanels(slot) {
462
+ // remove after d2l-tab/d2l-tab-panel backport
463
+ _getPanelsDefaultSlotBehavior(slot) {
530
464
  if (!slot) return;
531
465
  return slot.assignedElements({ flatten: true }).filter((node) => node.role === 'tabpanel');
532
466
  }
@@ -539,7 +473,7 @@ class Tabs extends LocalizeCoreElement(ArrowKeysMixin(SkeletonMixin(LitElement))
539
473
  async _handleDefaultSlotChange(e) {
540
474
  if (!this._defaultSlotBehavior) return;
541
475
 
542
- const panels = this._getPanels(e.target);
476
+ const panels = this._getPanelsDefaultSlotBehavior(e.target);
543
477
 
544
478
  // handle case where there are less than two tabs initially
545
479
  this._updateTabListVisibility(panels);
@@ -595,7 +529,7 @@ class Tabs extends LocalizeCoreElement(ArrowKeysMixin(SkeletonMixin(LitElement))
595
529
  if (!this._initialized && this._tabInfos.length > 0) {
596
530
 
597
531
  this._initialized = true;
598
- await this._updateTabsContainerWidth(selectedTabInfo);
532
+ await this._updateTabsContainerWidthDefaultSlotBehavior(selectedTabInfo);
599
533
 
600
534
  } else {
601
535
 
@@ -613,7 +547,7 @@ class Tabs extends LocalizeCoreElement(ArrowKeysMixin(SkeletonMixin(LitElement))
613
547
  if (selectedTabInfo) {
614
548
  Promise.all(animPromises).then(() => {
615
549
  this._updateMeasures();
616
- return this._updateScrollPosition(selectedTabInfo);
550
+ return this._updateScrollPositionDefaultSlotBehavior(selectedTabInfo);
617
551
  });
618
552
  }
619
553
 
@@ -637,9 +571,11 @@ class Tabs extends LocalizeCoreElement(ArrowKeysMixin(SkeletonMixin(LitElement))
637
571
  this.requestUpdate();
638
572
  }
639
573
 
640
- _handlePanelsSlotChange() {
574
+ _handlePanelsSlotChange(e) {
641
575
  if (this._defaultSlotBehavior) return;
642
576
 
577
+ this.#panels = e.target.assignedElements({ flatten: true }).filter((node) => node.role === 'tabpanel');
578
+ this.#checkTabPanelMatch();
643
579
  this.#setAriaControls();
644
580
  }
645
581
 
@@ -738,7 +674,7 @@ class Tabs extends LocalizeCoreElement(ArrowKeysMixin(SkeletonMixin(LitElement))
738
674
 
739
675
  }
740
676
 
741
- _handleTabSelected(e) {
677
+ async _handleTabSelected(e) {
742
678
  if (this._defaultSlotBehavior) {
743
679
  this._handleTabSelectedDefaultSlotBehavior(e);
744
680
  return;
@@ -746,6 +682,8 @@ class Tabs extends LocalizeCoreElement(ArrowKeysMixin(SkeletonMixin(LitElement))
746
682
 
747
683
  const selectedTab = e.target;
748
684
  this.#updateSelectedTab(selectedTab);
685
+ await this.updateComplete;
686
+ this._updateScrollPosition(selectedTab);
749
687
  }
750
688
 
751
689
  // remove after d2l-tab/d2l-tab-panel backport
@@ -758,7 +696,7 @@ class Tabs extends LocalizeCoreElement(ArrowKeysMixin(SkeletonMixin(LitElement))
758
696
  selectedTabInfo.activeFocusable = true;
759
697
 
760
698
  await this.updateComplete;
761
- this._updateScrollPosition(selectedTabInfo);
699
+ this._updateScrollPositionDefaultSlotBehavior(selectedTabInfo);
762
700
 
763
701
  selectedPanel.selected = true;
764
702
  this._tabInfos.forEach((tabInfo) => {
@@ -794,12 +732,19 @@ class Tabs extends LocalizeCoreElement(ArrowKeysMixin(SkeletonMixin(LitElement))
794
732
  if (selectedTab) {
795
733
  this.#updateSelectedTab(selectedTab);
796
734
  }
797
- this.#setAriaControls();
798
735
 
799
736
  await this.updateComplete;
737
+ this.#checkTabPanelMatch();
738
+ this.#setAriaControls();
800
739
 
801
740
  if (!this._initialized && this._tabs.length > 0) {
802
741
  this._initialized = true;
742
+ await this._updateTabsContainerWidth(selectedTab);
743
+ }
744
+
745
+ if (selectedTab) {
746
+ this._updateMeasures();
747
+ this._updateScrollPosition(selectedTab);
803
748
  }
804
749
  }
805
750
 
@@ -892,12 +837,15 @@ class Tabs extends LocalizeCoreElement(ArrowKeysMixin(SkeletonMixin(LitElement))
892
837
  _updateMeasures() {
893
838
  let totalTabsWidth = 0;
894
839
  if (!this.shadowRoot) return;
895
- const tabs = [...this.shadowRoot.querySelectorAll('d2l-tab-internal')];
840
+ const tabs = this._defaultSlotBehavior ? [...this.shadowRoot.querySelectorAll('d2l-tab-internal')] : this._tabs;
896
841
 
897
842
  const tabRects = tabs.map((tab) => {
843
+ const tabRect = tab.getBoundingClientRect();
844
+ const offsetLeft = this._defaultSlotBehavior ? tab.offsetLeft : getOffsetLeft(tab, tabRect);
845
+
898
846
  const measures = {
899
- rect: tab.getBoundingClientRect(),
900
- offsetLeft: tab.offsetLeft
847
+ rect: tabRect,
848
+ offsetLeft: offsetLeft
901
849
  };
902
850
  totalTabsWidth += measures.rect.width;
903
851
  return measures;
@@ -911,22 +859,17 @@ class Tabs extends LocalizeCoreElement(ArrowKeysMixin(SkeletonMixin(LitElement))
911
859
  };
912
860
  }
913
861
 
914
- _updateScrollPosition(selectedTabInfo) {
862
+ _updateScrollPosition(selectedTab) {
915
863
  const measures = this._getMeasures();
916
- const newTranslationValue = this._calculateScrollPosition(selectedTabInfo, measures);
917
- const scrollToPromise = this._scrollToPosition(newTranslationValue);
918
- const scrollVisibilityPromise = this._updateScrollVisibility(measures);
919
- const p = Promise.all([
920
- scrollVisibilityPromise,
921
- scrollToPromise
922
- ]);
923
- p.then(() => {
924
- if (this._loadingCompleteResolve) {
925
- this._loadingCompleteResolve();
926
- this._loadingCompleteResolve = undefined;
927
- }
928
- });
929
- return p;
864
+ const newTranslationValue = this._calculateScrollPosition(selectedTab, measures);
865
+ return this.#updateScrollPositionLogic(measures, newTranslationValue);
866
+ }
867
+
868
+ // remove after d2l-tab/d2l-tab-panel backport
869
+ _updateScrollPositionDefaultSlotBehavior(selectedTabInfo) {
870
+ const measures = this._getMeasures();
871
+ const newTranslationValue = this._calculateScrollPositionDefaultSlotBehavior(selectedTabInfo, measures);
872
+ return this.#updateScrollPositionLogic(measures, newTranslationValue);
930
873
  }
931
874
 
932
875
  _updateScrollVisibility(measures) {
@@ -977,28 +920,116 @@ class Tabs extends LocalizeCoreElement(ArrowKeysMixin(SkeletonMixin(LitElement))
977
920
  }
978
921
  }
979
922
 
980
- _updateTabsContainerWidth(selectedTabInfo) {
923
+ _updateTabsContainerWidth(selectedTab) {
924
+ const tabs = this._tabs;
925
+ if (!this.maxToShow || this.maxToShow <= 0 || this.maxToShow >= tabs.length) return;
926
+ if (tabs.indexOf(selectedTab) > this.maxToShow - 1) return;
927
+ return this.#updateTabsContainerWidthLogic();
928
+ }
929
+
930
+ // remove after d2l-tab/d2l-tab-panel backport
931
+ _updateTabsContainerWidthDefaultSlotBehavior(selectedTabInfo) {
981
932
  if (!this.maxToShow || this.maxToShow <= 0 || this.maxToShow >= this._tabInfos.length) return;
982
933
  if (this._tabInfos.indexOf(selectedTabInfo) > this.maxToShow - 1) return;
934
+ return this.#updateTabsContainerWidthLogic();
935
+ }
983
936
 
984
- const measures = this._getMeasures();
937
+ #calculateScrollPositionLogic(tabsDataStructure, selectedTabIndex, measures) {
938
+ if (!measures.tabRects[selectedTabIndex]) return 0;
985
939
 
986
- let maxWidth = 4; // initial value to allow for padding hack
987
- for (let i = 0; i < this.maxToShow; i++) {
988
- maxWidth += measures.tabRects[i].rect.width;
940
+ const selectedTabMeasures = measures.tabRects[selectedTabIndex];
941
+
942
+ const isOverflowingLeft = (selectedTabMeasures.offsetLeft + this._translationValue < 0);
943
+ const isOverflowingRight = (selectedTabMeasures.offsetLeft + selectedTabMeasures.rect.width + this._translationValue > measures.tabsContainerRect.width);
944
+
945
+ const isRTL = this.#isRTL();
946
+
947
+ let getNewTranslationValue;
948
+ if (!isRTL) {
949
+ getNewTranslationValue = () => {
950
+ if (selectedTabIndex === 0) {
951
+ // position selected tab at beginning
952
+ return 0;
953
+ } else if (selectedTabIndex === (tabsDataStructure.length - 1)) {
954
+ // position selected tab at end
955
+ return -1 * (selectedTabMeasures.offsetLeft - measures.tabsContainerRect.width + selectedTabMeasures.rect.width);
956
+ } else {
957
+ // position selected tab in middle
958
+ return -1 * (selectedTabMeasures.offsetLeft - (measures.tabsContainerRect.width / 2) + (selectedTabMeasures.rect.width / 2));
959
+ }
960
+ };
961
+ } else {
962
+ getNewTranslationValue = () => {
963
+ if (selectedTabIndex === 0) {
964
+ // position selected tab at beginning
965
+ return 0;
966
+ } else if (selectedTabIndex === (tabsDataStructure.length - 1)) {
967
+ // position selected tab at end
968
+ return -1 * selectedTabMeasures.offsetLeft;
969
+ } else {
970
+ // position selected tab in middle
971
+ return (measures.tabsContainerRect.width / 2) - (selectedTabMeasures.offsetLeft + selectedTabMeasures.rect.width / 2) + (selectedTabMeasures.rect.width / 2);
972
+ }
973
+ };
989
974
  }
990
975
 
991
- if (measures.tabsContainerListRect.width > maxWidth) {
992
- maxWidth += scrollButtonWidth;
976
+ let newTranslationValue = this._translationValue;
977
+ if (isOverflowingLeft || isOverflowingRight) {
978
+ newTranslationValue = getNewTranslationValue();
993
979
  }
994
980
 
995
- if (maxWidth >= measures.tabsContainerRect.width) return;
981
+ let expectedPosition;
996
982
 
997
- this._maxWidth = maxWidth;
998
- this._scrollCollapsed = true;
999
- this._measures = null;
983
+ // make sure the new position will not place selected tab behind left scroll button
984
+ if (!isRTL) {
985
+ expectedPosition = selectedTabMeasures.offsetLeft + newTranslationValue;
986
+ if (newTranslationValue < 0 && this._isPositionInLeftScrollArea(expectedPosition)) {
987
+ newTranslationValue = getNewTranslationValue();
988
+ }
989
+ } else {
990
+ expectedPosition = selectedTabMeasures.offsetLeft + selectedTabMeasures.rect.width + newTranslationValue;
991
+ if (newTranslationValue > 0 && this._isPositionInRightScrollArea(expectedPosition, measures)) {
992
+ newTranslationValue = getNewTranslationValue();
993
+ }
994
+ }
1000
995
 
1001
- return this.updateComplete;
996
+ if (!isRTL) {
997
+ // make sure there will not be any empty space between left side of container and first tab
998
+ if (newTranslationValue > 0) newTranslationValue = 0;
999
+ } else {
1000
+ // make sure there will not be any empty space between right side of container and first tab
1001
+ if (newTranslationValue < 0) newTranslationValue = 0;
1002
+ }
1003
+
1004
+ // make sure the new position will not place selected tab behind the right scroll button
1005
+ if (!isRTL) {
1006
+ expectedPosition = selectedTabMeasures.offsetLeft + selectedTabMeasures.rect.width + newTranslationValue;
1007
+ if ((selectedTabIndex < tabsDataStructure.length - 1) && this._isPositionInRightScrollArea(expectedPosition, measures)) {
1008
+ newTranslationValue = getNewTranslationValue();
1009
+ }
1010
+ } else {
1011
+ expectedPosition = selectedTabMeasures.offsetLeft + newTranslationValue;
1012
+ if ((selectedTabIndex < tabsDataStructure.length - 1) && this._isPositionInLeftScrollArea(expectedPosition)) {
1013
+ newTranslationValue = getNewTranslationValue();
1014
+ }
1015
+ }
1016
+
1017
+ return newTranslationValue;
1018
+ }
1019
+
1020
+ #checkTabPanelMatch() {
1021
+ // debounce so only runs once when tabs/panels slots changing
1022
+ if (this.#checkTabPanelMatchRequested) return;
1023
+
1024
+ this.#checkTabPanelMatchRequested = true;
1025
+ setTimeout(() => {
1026
+ if ((this._tabs && !this.#panels) || (this.#panels && !this._tabs)) {
1027
+ console.warn('d2l-tabs: tabs and panels are not in sync');
1028
+ } else if (this._tabs.length !== this.#panels.length) {
1029
+ console.warn('d2l-tabs: number of tabs and panels does not match');
1030
+ }
1031
+ this.#checkTabPanelMatchRequested = false;
1032
+ }, 0);
1002
1033
  }
1003
1034
 
1004
1035
  #isRTL() {
@@ -1007,26 +1038,45 @@ class Tabs extends LocalizeCoreElement(ArrowKeysMixin(SkeletonMixin(LitElement))
1007
1038
 
1008
1039
  #setAriaControls() {
1009
1040
  // debounce so only runs once when tabs/panels slots changing
1010
- if (this._updateAriaControlsRequested) return;
1041
+ if (this.#updateAriaControlsRequested) return;
1011
1042
 
1012
- this._updateAriaControlsRequested = true;
1043
+ this.#updateAriaControlsRequested = true;
1013
1044
  setTimeout(() => {
1014
1045
  this._tabs?.forEach((tab) => {
1015
1046
  const panel = this._getPanel(tab.id);
1016
- if (!panel) return;
1047
+ if (!panel) {
1048
+ console.warn('d2l-tabs: tab without matching panel');
1049
+ return;
1050
+ }
1017
1051
  tab.setAttribute('aria-controls', `${panel.id}`);
1018
1052
  });
1019
- this._updateAriaControlsRequested = false;
1053
+ this.#updateAriaControlsRequested = false;
1020
1054
  }, 0);
1021
1055
  }
1022
1056
 
1057
+ #updateScrollPositionLogic(measures, newTranslationValue) {
1058
+ const scrollToPromise = this._scrollToPosition(newTranslationValue);
1059
+ const scrollVisibilityPromise = this._updateScrollVisibility(measures);
1060
+ const p = Promise.all([
1061
+ scrollVisibilityPromise,
1062
+ scrollToPromise
1063
+ ]);
1064
+ p.then(() => {
1065
+ if (this._loadingCompleteResolve) {
1066
+ this._loadingCompleteResolve();
1067
+ this._loadingCompleteResolve = undefined;
1068
+ }
1069
+ });
1070
+ return p;
1071
+ }
1072
+
1023
1073
  async #updateSelectedTab(selectedTab) {
1024
- const selectedPanel = this._getPanel(selectedTab.id);
1025
1074
  selectedTab.tabIndex = 0;
1026
1075
 
1027
1076
  await this.updateComplete;
1028
1077
 
1029
- selectedPanel.selected = true;
1078
+ const selectedPanel = this._getPanel(selectedTab.id);
1079
+ if (selectedPanel) selectedPanel.selected = true;
1030
1080
  this._tabs.forEach((tab) => {
1031
1081
  if (tab.id !== selectedTab.id) {
1032
1082
  if (tab.selected) {
@@ -1040,6 +1090,26 @@ class Tabs extends LocalizeCoreElement(ArrowKeysMixin(SkeletonMixin(LitElement))
1040
1090
  });
1041
1091
  }
1042
1092
 
1093
+ #updateTabsContainerWidthLogic() {
1094
+ const measures = this._getMeasures();
1095
+
1096
+ let maxWidth = 4; // initial value to allow for padding hack
1097
+ for (let i = 0; i < this.maxToShow; i++) {
1098
+ maxWidth += measures.tabRects[i].rect.width;
1099
+ }
1100
+
1101
+ if (measures.tabsContainerListRect.width > maxWidth) {
1102
+ maxWidth += scrollButtonWidth;
1103
+ }
1104
+
1105
+ if (maxWidth >= measures.tabsContainerRect.width) return;
1106
+
1107
+ this._maxWidth = maxWidth;
1108
+ this._scrollCollapsed = true;
1109
+ this._measures = null;
1110
+
1111
+ return this.updateComplete;
1112
+ }
1043
1113
  }
1044
1114
 
1045
1115
  customElements.define('d2l-tabs', Tabs);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brightspace-ui/core",
3
- "version": "3.94.0",
3
+ "version": "3.94.2",
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",