@brightspace-ui/core 3.93.2 → 3.94.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.
@@ -40,13 +40,31 @@
40
40
 
41
41
  <d2l-demo-snippet>
42
42
  <template>
43
- <d2l-tabs>
43
+ <d2l-tabs text="Courses">
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-panel labelled-by="all" slot="panels">Tab content for All</d2l-tab-panel>
48
- <d2l-tab-panel labelled-by="biology" slot="panels">Tab content for Biology</d2l-tab-panel>
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>
50
+ <d2l-tab-panel labelled-by="all" slot="panels" id="all-panel">Tab content for All</d2l-tab-panel>
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,11 +3,12 @@ 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';
10
10
  import { getFocusPseudoClass } from '../../helpers/focus.js';
11
+ import { ifDefined } from 'lit/directives/if-defined.js';
11
12
  import { LocalizeCoreElement } from '../../helpers/localize-core-element.js';
12
13
  import { repeat } from 'lit/directives/repeat.js';
13
14
  import ResizeObserver from 'resize-observer-polyfill/dist/ResizeObserver.es.js';
@@ -18,6 +19,11 @@ const reduceMotion = matchMedia('(prefers-reduced-motion: reduce)').matches;
18
19
 
19
20
  const scrollButtonWidth = 56;
20
21
 
22
+ function getOffsetLeft(tab, tabRect) {
23
+ const offsetParent = getOffsetParent(tab);
24
+ return Math.round(tabRect.left - offsetParent.getBoundingClientRect().left);
25
+ }
26
+
21
27
  /**
22
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.
23
29
  * @slot - Contains the tab panels (e.g., "d2l-tab-panel" components)
@@ -32,6 +38,11 @@ class Tabs extends LocalizeCoreElement(ArrowKeysMixin(SkeletonMixin(LitElement))
32
38
  * @type {number}
33
39
  */
34
40
  maxToShow: { type: Number, attribute: 'max-to-show' },
41
+ /**
42
+ * REQUIRED: ACCESSIBILITY: Accessible text for the tablist
43
+ * @type {string}
44
+ */
45
+ text: { type: String },
35
46
  _allowScrollNext: { type: Boolean },
36
47
  _allowScrollPrevious: { type: Boolean },
37
48
  _defaultSlotBehavior: { state: true },
@@ -88,6 +99,7 @@ class Tabs extends LocalizeCoreElement(ArrowKeysMixin(SkeletonMixin(LitElement))
88
99
  }
89
100
  .d2l-tabs-container-list {
90
101
  display: flex;
102
+ position: relative;
91
103
  -webkit-transition: transform 200ms ease-out;
92
104
  transition: transform 200ms ease-out;
93
105
  white-space: nowrap;
@@ -241,10 +253,10 @@ class Tabs extends LocalizeCoreElement(ArrowKeysMixin(SkeletonMixin(LitElement))
241
253
  await this.updateComplete;
242
254
 
243
255
  if (!this._scrollCollapsed) {
244
- return this._updateScrollPosition(tabInfo);
256
+ return this._updateScrollPositionDefaultSlotBehavior(tabInfo);
245
257
  } else {
246
258
  const measures = this._getMeasures();
247
- const newTranslationValue = this._calculateScrollPosition(tabInfo, measures);
259
+ const newTranslationValue = this._calculateScrollPositionDefaultSlotBehavior(tabInfo, measures);
248
260
 
249
261
  if (!this.#isRTL()) {
250
262
  if (newTranslationValue >= 0) return;
@@ -256,7 +268,7 @@ class Tabs extends LocalizeCoreElement(ArrowKeysMixin(SkeletonMixin(LitElement))
256
268
  if (expanded) {
257
269
  return;
258
270
  } else {
259
- return this._updateScrollPosition(tabInfo);
271
+ return this._updateScrollPositionDefaultSlotBehavior(tabInfo);
260
272
  }
261
273
  }
262
274
  };
@@ -305,6 +317,7 @@ class Tabs extends LocalizeCoreElement(ArrowKeysMixin(SkeletonMixin(LitElement))
305
317
  <div class="d2l-tabs-container-list"
306
318
  @d2l-tab-selected="${this._handleTabSelected}"
307
319
  @focusout="${this._handleFocusOut}"
320
+ aria-label="${ifDefined(this.text)}"
308
321
  role="tablist"
309
322
  style="${styleMap(tabsContainerListStyles)}">
310
323
  ${repeat(this._tabInfos, (tabInfo) => tabInfo.id, (tabInfo) => html`
@@ -333,7 +346,7 @@ class Tabs extends LocalizeCoreElement(ArrowKeysMixin(SkeletonMixin(LitElement))
333
346
  @d2l-tab-panel-selected="${this._handlePanelSelected}"
334
347
  @d2l-tab-panel-text-changed="${this._handlePanelTextChange}">
335
348
  <slot @slotchange="${this._handleDefaultSlotChange}"></slot>
336
- <slot name="panels"></slot>
349
+ <slot name="panels" @slotchange="${this._handlePanelsSlotChange}"></slot>
337
350
  </div>
338
351
  `;
339
352
  }
@@ -343,7 +356,7 @@ class Tabs extends LocalizeCoreElement(ArrowKeysMixin(SkeletonMixin(LitElement))
343
356
  }
344
357
 
345
358
  async getLoadingComplete() {
346
- return this._defaultSlotBehavior ? this._loadingCompletePromise : true;
359
+ return this._loadingCompletePromise;
347
360
  }
348
361
 
349
362
  getTabListRect() {
@@ -381,91 +394,16 @@ class Tabs extends LocalizeCoreElement(ArrowKeysMixin(SkeletonMixin(LitElement))
381
394
  });
382
395
  }
383
396
 
384
- _calculateScrollPosition(selectedTabInfo, measures) {
397
+ _calculateScrollPosition(selectedTab, measures) {
398
+ const tabs = this._tabs;
399
+ const selectedTabIndex = tabs.indexOf(selectedTab);
400
+ return this.#calculateScrollPositionLogic(tabs, selectedTabIndex, measures);
401
+ }
385
402
 
403
+ // remove after d2l-tab/d2l-tab-panel backport
404
+ _calculateScrollPositionDefaultSlotBehavior(selectedTabInfo, measures) {
386
405
  const selectedTabIndex = this._tabInfos.indexOf(selectedTabInfo);
387
-
388
- if (!measures.tabRects[selectedTabIndex]) return 0;
389
-
390
- const selectedTabMeasures = measures.tabRects[selectedTabIndex];
391
-
392
- const isOverflowingLeft = (selectedTabMeasures.offsetLeft + this._translationValue < 0);
393
- const isOverflowingRight = (selectedTabMeasures.offsetLeft + selectedTabMeasures.rect.width + this._translationValue > measures.tabsContainerRect.width);
394
-
395
- const isRTL = this.#isRTL();
396
-
397
- let getNewTranslationValue;
398
- if (!isRTL) {
399
- getNewTranslationValue = () => {
400
- if (selectedTabIndex === 0) {
401
- // position selected tab at beginning
402
- return 0;
403
- } else if (selectedTabIndex === (this._tabInfos.length - 1)) {
404
- // position selected tab at end
405
- return -1 * (selectedTabMeasures.offsetLeft - measures.tabsContainerRect.width + selectedTabMeasures.rect.width);
406
- } else {
407
- // position selected tab in middle
408
- return -1 * (selectedTabMeasures.offsetLeft - (measures.tabsContainerRect.width / 2) + (selectedTabMeasures.rect.width / 2));
409
- }
410
- };
411
- } else {
412
- getNewTranslationValue = () => {
413
- if (selectedTabIndex === 0) {
414
- // position selected tab at beginning
415
- return 0;
416
- } else if (selectedTabIndex === (this._tabInfos.length - 1)) {
417
- // position selected tab at end
418
- return -1 * selectedTabMeasures.offsetLeft;
419
- } else {
420
- // position selected tab in middle
421
- return (measures.tabsContainerRect.width / 2) - (selectedTabMeasures.offsetLeft + selectedTabMeasures.rect.width / 2) + (selectedTabMeasures.rect.width / 2);
422
- }
423
- };
424
- }
425
-
426
- let newTranslationValue = this._translationValue;
427
- if (isOverflowingLeft || isOverflowingRight) {
428
- newTranslationValue = getNewTranslationValue();
429
- }
430
-
431
- let expectedPosition;
432
-
433
- // make sure the new position will not place selected tab behind left scroll button
434
- if (!isRTL) {
435
- expectedPosition = selectedTabMeasures.offsetLeft + newTranslationValue;
436
- if (newTranslationValue < 0 && this._isPositionInLeftScrollArea(expectedPosition)) {
437
- newTranslationValue = getNewTranslationValue();
438
- }
439
- } else {
440
- expectedPosition = selectedTabMeasures.offsetLeft + selectedTabMeasures.rect.width + newTranslationValue;
441
- if (newTranslationValue > 0 && this._isPositionInRightScrollArea(expectedPosition, measures)) {
442
- newTranslationValue = getNewTranslationValue();
443
- }
444
- }
445
-
446
- if (!isRTL) {
447
- // make sure there will not be any empty space between left side of container and first tab
448
- if (newTranslationValue > 0) newTranslationValue = 0;
449
- } else {
450
- // make sure there will not be any empty space between right side of container and first tab
451
- if (newTranslationValue < 0) newTranslationValue = 0;
452
- }
453
-
454
- // make sure the new position will not place selected tab behind the right scroll button
455
- if (!isRTL) {
456
- expectedPosition = selectedTabMeasures.offsetLeft + selectedTabMeasures.rect.width + newTranslationValue;
457
- if ((selectedTabIndex < this._tabInfos.length - 1) && this._isPositionInRightScrollArea(expectedPosition, measures)) {
458
- newTranslationValue = getNewTranslationValue();
459
- }
460
- } else {
461
- expectedPosition = selectedTabMeasures.offsetLeft + newTranslationValue;
462
- if ((selectedTabIndex < this._tabInfos.length - 1) && this._isPositionInLeftScrollArea(expectedPosition)) {
463
- newTranslationValue = getNewTranslationValue();
464
- }
465
- }
466
-
467
- return newTranslationValue;
468
-
406
+ return this.#calculateScrollPositionLogic(this._tabInfos, selectedTabIndex, measures);
469
407
  }
470
408
 
471
409
  async _focusSelected() {
@@ -473,7 +411,7 @@ class Tabs extends LocalizeCoreElement(ArrowKeysMixin(SkeletonMixin(LitElement))
473
411
  if (!selectedTab) return;
474
412
 
475
413
  const selectedTabInfo = this._getTabInfo(selectedTab.controlsPanel);
476
- await this._updateScrollPosition(selectedTabInfo);
414
+ await this._updateScrollPositionDefaultSlotBehavior(selectedTabInfo);
477
415
 
478
416
  selectedTab.focus();
479
417
  }
@@ -588,7 +526,7 @@ class Tabs extends LocalizeCoreElement(ArrowKeysMixin(SkeletonMixin(LitElement))
588
526
  if (!this._initialized && this._tabInfos.length > 0) {
589
527
 
590
528
  this._initialized = true;
591
- await this._updateTabsContainerWidth(selectedTabInfo);
529
+ await this._updateTabsContainerWidthDefaultSlotBehavior(selectedTabInfo);
592
530
 
593
531
  } else {
594
532
 
@@ -606,7 +544,7 @@ class Tabs extends LocalizeCoreElement(ArrowKeysMixin(SkeletonMixin(LitElement))
606
544
  if (selectedTabInfo) {
607
545
  Promise.all(animPromises).then(() => {
608
546
  this._updateMeasures();
609
- return this._updateScrollPosition(selectedTabInfo);
547
+ return this._updateScrollPositionDefaultSlotBehavior(selectedTabInfo);
610
548
  });
611
549
  }
612
550
 
@@ -630,6 +568,12 @@ class Tabs extends LocalizeCoreElement(ArrowKeysMixin(SkeletonMixin(LitElement))
630
568
  this.requestUpdate();
631
569
  }
632
570
 
571
+ _handlePanelsSlotChange() {
572
+ if (this._defaultSlotBehavior) return;
573
+
574
+ this.#setAriaControls();
575
+ }
576
+
633
577
  async _handlePanelTextChange(e) {
634
578
  const tabInfo = this._getTabInfo(e.target.id);
635
579
  // event could be from nested tabs
@@ -725,7 +669,7 @@ class Tabs extends LocalizeCoreElement(ArrowKeysMixin(SkeletonMixin(LitElement))
725
669
 
726
670
  }
727
671
 
728
- _handleTabSelected(e) {
672
+ async _handleTabSelected(e) {
729
673
  if (this._defaultSlotBehavior) {
730
674
  this._handleTabSelectedDefaultSlotBehavior(e);
731
675
  return;
@@ -733,6 +677,8 @@ class Tabs extends LocalizeCoreElement(ArrowKeysMixin(SkeletonMixin(LitElement))
733
677
 
734
678
  const selectedTab = e.target;
735
679
  this.#updateSelectedTab(selectedTab);
680
+ await this.updateComplete;
681
+ this._updateScrollPosition(selectedTab);
736
682
  }
737
683
 
738
684
  // remove after d2l-tab/d2l-tab-panel backport
@@ -745,7 +691,7 @@ class Tabs extends LocalizeCoreElement(ArrowKeysMixin(SkeletonMixin(LitElement))
745
691
  selectedTabInfo.activeFocusable = true;
746
692
 
747
693
  await this.updateComplete;
748
- this._updateScrollPosition(selectedTabInfo);
694
+ this._updateScrollPositionDefaultSlotBehavior(selectedTabInfo);
749
695
 
750
696
  selectedPanel.selected = true;
751
697
  this._tabInfos.forEach((tabInfo) => {
@@ -781,17 +727,18 @@ class Tabs extends LocalizeCoreElement(ArrowKeysMixin(SkeletonMixin(LitElement))
781
727
  if (selectedTab) {
782
728
  this.#updateSelectedTab(selectedTab);
783
729
  }
730
+ this.#setAriaControls();
784
731
 
785
732
  await this.updateComplete;
786
733
 
787
734
  if (!this._initialized && this._tabs.length > 0) {
788
735
  this._initialized = true;
736
+ await this._updateTabsContainerWidth(selectedTab);
789
737
  }
790
738
 
791
739
  if (selectedTab) {
792
- // set corresponding panel to selected
793
- const selectedPanel = this._getPanel(selectedTab.id);
794
- if (selectedPanel) selectedPanel.selected = true;
740
+ this._updateMeasures();
741
+ this._updateScrollPosition(selectedTab);
795
742
  }
796
743
  }
797
744
 
@@ -884,12 +831,15 @@ class Tabs extends LocalizeCoreElement(ArrowKeysMixin(SkeletonMixin(LitElement))
884
831
  _updateMeasures() {
885
832
  let totalTabsWidth = 0;
886
833
  if (!this.shadowRoot) return;
887
- const tabs = [...this.shadowRoot.querySelectorAll('d2l-tab-internal')];
834
+ const tabs = this._defaultSlotBehavior ? [...this.shadowRoot.querySelectorAll('d2l-tab-internal')] : this._tabs;
888
835
 
889
836
  const tabRects = tabs.map((tab) => {
837
+ const tabRect = tab.getBoundingClientRect();
838
+ const offsetLeft = this._defaultSlotBehavior ? tab.offsetLeft : getOffsetLeft(tab, tabRect);
839
+
890
840
  const measures = {
891
- rect: tab.getBoundingClientRect(),
892
- offsetLeft: tab.offsetLeft
841
+ rect: tabRect,
842
+ offsetLeft: offsetLeft
893
843
  };
894
844
  totalTabsWidth += measures.rect.width;
895
845
  return measures;
@@ -903,22 +853,17 @@ class Tabs extends LocalizeCoreElement(ArrowKeysMixin(SkeletonMixin(LitElement))
903
853
  };
904
854
  }
905
855
 
906
- _updateScrollPosition(selectedTabInfo) {
856
+ _updateScrollPosition(selectedTab) {
907
857
  const measures = this._getMeasures();
908
- const newTranslationValue = this._calculateScrollPosition(selectedTabInfo, measures);
909
- const scrollToPromise = this._scrollToPosition(newTranslationValue);
910
- const scrollVisibilityPromise = this._updateScrollVisibility(measures);
911
- const p = Promise.all([
912
- scrollVisibilityPromise,
913
- scrollToPromise
914
- ]);
915
- p.then(() => {
916
- if (this._loadingCompleteResolve) {
917
- this._loadingCompleteResolve();
918
- this._loadingCompleteResolve = undefined;
919
- }
920
- });
921
- return p;
858
+ const newTranslationValue = this._calculateScrollPosition(selectedTab, measures);
859
+ return this.#updateScrollPositionLogic(measures, newTranslationValue);
860
+ }
861
+
862
+ // remove after d2l-tab/d2l-tab-panel backport
863
+ _updateScrollPositionDefaultSlotBehavior(selectedTabInfo) {
864
+ const measures = this._getMeasures();
865
+ const newTranslationValue = this._calculateScrollPositionDefaultSlotBehavior(selectedTabInfo, measures);
866
+ return this.#updateScrollPositionLogic(measures, newTranslationValue);
922
867
  }
923
868
 
924
869
  _updateScrollVisibility(measures) {
@@ -969,34 +914,138 @@ class Tabs extends LocalizeCoreElement(ArrowKeysMixin(SkeletonMixin(LitElement))
969
914
  }
970
915
  }
971
916
 
972
- _updateTabsContainerWidth(selectedTabInfo) {
917
+ _updateTabsContainerWidth(selectedTab) {
918
+ const tabs = this._tabs;
919
+ if (!this.maxToShow || this.maxToShow <= 0 || this.maxToShow >= tabs.length) return;
920
+ if (tabs.indexOf(selectedTab) > this.maxToShow - 1) return;
921
+ return this.#updateTabsContainerWidthLogic();
922
+ }
923
+
924
+ // remove after d2l-tab/d2l-tab-panel backport
925
+ _updateTabsContainerWidthDefaultSlotBehavior(selectedTabInfo) {
973
926
  if (!this.maxToShow || this.maxToShow <= 0 || this.maxToShow >= this._tabInfos.length) return;
974
927
  if (this._tabInfos.indexOf(selectedTabInfo) > this.maxToShow - 1) return;
928
+ return this.#updateTabsContainerWidthLogic();
929
+ }
975
930
 
976
- const measures = this._getMeasures();
931
+ #calculateScrollPositionLogic(tabsDataStructure, selectedTabIndex, measures) {
932
+ if (!measures.tabRects[selectedTabIndex]) return 0;
977
933
 
978
- let maxWidth = 4; // initial value to allow for padding hack
979
- for (let i = 0; i < this.maxToShow; i++) {
980
- maxWidth += measures.tabRects[i].rect.width;
934
+ const selectedTabMeasures = measures.tabRects[selectedTabIndex];
935
+
936
+ const isOverflowingLeft = (selectedTabMeasures.offsetLeft + this._translationValue < 0);
937
+ const isOverflowingRight = (selectedTabMeasures.offsetLeft + selectedTabMeasures.rect.width + this._translationValue > measures.tabsContainerRect.width);
938
+
939
+ const isRTL = this.#isRTL();
940
+
941
+ let getNewTranslationValue;
942
+ if (!isRTL) {
943
+ getNewTranslationValue = () => {
944
+ if (selectedTabIndex === 0) {
945
+ // position selected tab at beginning
946
+ return 0;
947
+ } else if (selectedTabIndex === (tabsDataStructure.length - 1)) {
948
+ // position selected tab at end
949
+ return -1 * (selectedTabMeasures.offsetLeft - measures.tabsContainerRect.width + selectedTabMeasures.rect.width);
950
+ } else {
951
+ // position selected tab in middle
952
+ return -1 * (selectedTabMeasures.offsetLeft - (measures.tabsContainerRect.width / 2) + (selectedTabMeasures.rect.width / 2));
953
+ }
954
+ };
955
+ } else {
956
+ getNewTranslationValue = () => {
957
+ if (selectedTabIndex === 0) {
958
+ // position selected tab at beginning
959
+ return 0;
960
+ } else if (selectedTabIndex === (tabsDataStructure.length - 1)) {
961
+ // position selected tab at end
962
+ return -1 * selectedTabMeasures.offsetLeft;
963
+ } else {
964
+ // position selected tab in middle
965
+ return (measures.tabsContainerRect.width / 2) - (selectedTabMeasures.offsetLeft + selectedTabMeasures.rect.width / 2) + (selectedTabMeasures.rect.width / 2);
966
+ }
967
+ };
981
968
  }
982
969
 
983
- if (measures.tabsContainerListRect.width > maxWidth) {
984
- maxWidth += scrollButtonWidth;
970
+ let newTranslationValue = this._translationValue;
971
+ if (isOverflowingLeft || isOverflowingRight) {
972
+ newTranslationValue = getNewTranslationValue();
985
973
  }
986
974
 
987
- if (maxWidth >= measures.tabsContainerRect.width) return;
975
+ let expectedPosition;
988
976
 
989
- this._maxWidth = maxWidth;
990
- this._scrollCollapsed = true;
991
- this._measures = null;
977
+ // make sure the new position will not place selected tab behind left scroll button
978
+ if (!isRTL) {
979
+ expectedPosition = selectedTabMeasures.offsetLeft + newTranslationValue;
980
+ if (newTranslationValue < 0 && this._isPositionInLeftScrollArea(expectedPosition)) {
981
+ newTranslationValue = getNewTranslationValue();
982
+ }
983
+ } else {
984
+ expectedPosition = selectedTabMeasures.offsetLeft + selectedTabMeasures.rect.width + newTranslationValue;
985
+ if (newTranslationValue > 0 && this._isPositionInRightScrollArea(expectedPosition, measures)) {
986
+ newTranslationValue = getNewTranslationValue();
987
+ }
988
+ }
992
989
 
993
- return this.updateComplete;
990
+ if (!isRTL) {
991
+ // make sure there will not be any empty space between left side of container and first tab
992
+ if (newTranslationValue > 0) newTranslationValue = 0;
993
+ } else {
994
+ // make sure there will not be any empty space between right side of container and first tab
995
+ if (newTranslationValue < 0) newTranslationValue = 0;
996
+ }
997
+
998
+ // make sure the new position will not place selected tab behind the right scroll button
999
+ if (!isRTL) {
1000
+ expectedPosition = selectedTabMeasures.offsetLeft + selectedTabMeasures.rect.width + newTranslationValue;
1001
+ if ((selectedTabIndex < tabsDataStructure.length - 1) && this._isPositionInRightScrollArea(expectedPosition, measures)) {
1002
+ newTranslationValue = getNewTranslationValue();
1003
+ }
1004
+ } else {
1005
+ expectedPosition = selectedTabMeasures.offsetLeft + newTranslationValue;
1006
+ if ((selectedTabIndex < tabsDataStructure.length - 1) && this._isPositionInLeftScrollArea(expectedPosition)) {
1007
+ newTranslationValue = getNewTranslationValue();
1008
+ }
1009
+ }
1010
+
1011
+ return newTranslationValue;
994
1012
  }
995
1013
 
996
1014
  #isRTL() {
997
1015
  return document.documentElement.getAttribute('dir') === 'rtl';
998
1016
  }
999
1017
 
1018
+ #setAriaControls() {
1019
+ // debounce so only runs once when tabs/panels slots changing
1020
+ if (this._updateAriaControlsRequested) return;
1021
+
1022
+ this._updateAriaControlsRequested = true;
1023
+ setTimeout(() => {
1024
+ this._tabs?.forEach((tab) => {
1025
+ const panel = this._getPanel(tab.id);
1026
+ if (!panel) return;
1027
+ tab.setAttribute('aria-controls', `${panel.id}`);
1028
+ });
1029
+ this._updateAriaControlsRequested = false;
1030
+ }, 0);
1031
+ }
1032
+
1033
+ #updateScrollPositionLogic(measures, newTranslationValue) {
1034
+ const scrollToPromise = this._scrollToPosition(newTranslationValue);
1035
+ const scrollVisibilityPromise = this._updateScrollVisibility(measures);
1036
+ const p = Promise.all([
1037
+ scrollVisibilityPromise,
1038
+ scrollToPromise
1039
+ ]);
1040
+ p.then(() => {
1041
+ if (this._loadingCompleteResolve) {
1042
+ this._loadingCompleteResolve();
1043
+ this._loadingCompleteResolve = undefined;
1044
+ }
1045
+ });
1046
+ return p;
1047
+ }
1048
+
1000
1049
  async #updateSelectedTab(selectedTab) {
1001
1050
  const selectedPanel = this._getPanel(selectedTab.id);
1002
1051
  selectedTab.tabIndex = 0;
@@ -1017,6 +1066,26 @@ class Tabs extends LocalizeCoreElement(ArrowKeysMixin(SkeletonMixin(LitElement))
1017
1066
  });
1018
1067
  }
1019
1068
 
1069
+ #updateTabsContainerWidthLogic() {
1070
+ const measures = this._getMeasures();
1071
+
1072
+ let maxWidth = 4; // initial value to allow for padding hack
1073
+ for (let i = 0; i < this.maxToShow; i++) {
1074
+ maxWidth += measures.tabRects[i].rect.width;
1075
+ }
1076
+
1077
+ if (measures.tabsContainerListRect.width > maxWidth) {
1078
+ maxWidth += scrollButtonWidth;
1079
+ }
1080
+
1081
+ if (maxWidth >= measures.tabsContainerRect.width) return;
1082
+
1083
+ this._maxWidth = maxWidth;
1084
+ this._scrollCollapsed = true;
1085
+ this._measures = null;
1086
+
1087
+ return this.updateComplete;
1088
+ }
1020
1089
  }
1021
1090
 
1022
1091
  customElements.define('d2l-tabs', Tabs);
@@ -12905,6 +12905,11 @@
12905
12905
  "path": "./components/tabs/tabs.js",
12906
12906
  "description": "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.",
12907
12907
  "attributes": [
12908
+ {
12909
+ "name": "text",
12910
+ "description": "REQUIRED: ACCESSIBILITY: Accessible text for the tablist",
12911
+ "type": "string"
12912
+ },
12908
12913
  {
12909
12914
  "name": "max-to-show",
12910
12915
  "description": "Limit the number of tabs to initially display",
@@ -12918,6 +12923,12 @@
12918
12923
  }
12919
12924
  ],
12920
12925
  "properties": [
12926
+ {
12927
+ "name": "text",
12928
+ "attribute": "text",
12929
+ "description": "REQUIRED: ACCESSIBILITY: Accessible text for the tablist",
12930
+ "type": "string"
12931
+ },
12921
12932
  {
12922
12933
  "name": "maxToShow",
12923
12934
  "attribute": "max-to-show",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brightspace-ui/core",
3
- "version": "3.93.2",
3
+ "version": "3.94.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",