@undrr/undrr-mangrove 1.3.3 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (155) hide show
  1. package/README.md +32 -25
  2. package/components/BarChart.js +2 -2
  3. package/components/BookCard.js +6 -0
  4. package/components/BookCard.js.LICENSE.txt +11 -0
  5. package/components/Breadcrumbs.js +6 -0
  6. package/components/Breadcrumbs.js.LICENSE.txt +9 -0
  7. package/components/Checkbox.js +6 -0
  8. package/components/Checkbox.js.LICENSE.txt +9 -0
  9. package/components/Chips.js +6 -0
  10. package/components/Chips.js.LICENSE.txt +9 -0
  11. package/components/CtaButton.js +6 -0
  12. package/components/CtaButton.js.LICENSE.txt +9 -0
  13. package/components/EmbedContainer.js +6 -0
  14. package/components/EmbedContainer.js.LICENSE.txt +9 -0
  15. package/components/Fetcher.js +2 -2
  16. package/components/Footer.js +6 -0
  17. package/components/Footer.js.LICENSE.txt +9 -0
  18. package/components/FormErrorSummary.js +6 -0
  19. package/components/FormErrorSummary.js.LICENSE.txt +9 -0
  20. package/components/FormGroup.js +6 -0
  21. package/components/FormGroup.js.LICENSE.txt +9 -0
  22. package/components/FullWidth.js +6 -0
  23. package/components/FullWidth.js.LICENSE.txt +9 -0
  24. package/components/Gallery.js +1 -1
  25. package/components/Hero.js +6 -0
  26. package/components/Hero.js.LICENSE.txt +9 -0
  27. package/components/HighlightBox.js +6 -0
  28. package/components/HighlightBox.js.LICENSE.txt +9 -0
  29. package/components/HorizontalBookCard.js +6 -0
  30. package/components/HorizontalBookCard.js.LICENSE.txt +11 -0
  31. package/components/HorizontalCard.js +6 -0
  32. package/components/HorizontalCard.js.LICENSE.txt +11 -0
  33. package/components/IconCard.js +2 -2
  34. package/components/Loader.js +6 -0
  35. package/components/Loader.js.LICENSE.txt +9 -0
  36. package/components/MapComponent.js +2 -2
  37. package/components/MegaMenu.js +2 -2
  38. package/components/PageHeader.js +6 -0
  39. package/components/PageHeader.js.LICENSE.txt +9 -0
  40. package/components/Pager.js +2 -2
  41. package/components/QuoteHighlight.js +1 -1
  42. package/components/Radio.js +6 -0
  43. package/components/Radio.js.LICENSE.txt +9 -0
  44. package/components/ScrollContainer.js +1 -1
  45. package/components/Select.js +6 -0
  46. package/components/Select.js.LICENSE.txt +9 -0
  47. package/components/ShareButtons.js +1 -1
  48. package/components/ShowMore.js +6 -0
  49. package/components/ShowMore.js.LICENSE.txt +9 -0
  50. package/components/StatsCard.js +2 -2
  51. package/components/SyndicationSearchWidget.js +2 -2
  52. package/components/Tab.js +6 -0
  53. package/components/Tab.js.LICENSE.txt +9 -0
  54. package/components/TextCta.js +6 -0
  55. package/components/TextCta.js.LICENSE.txt +11 -0
  56. package/components/TextInput.js +6 -0
  57. package/components/TextInput.js.LICENSE.txt +9 -0
  58. package/components/Textarea.js +6 -0
  59. package/components/Textarea.js.LICENSE.txt +9 -0
  60. package/components/VerticalCard.js +6 -0
  61. package/components/VerticalCard.js.LICENSE.txt +11 -0
  62. package/components/hydrate.js +1 -1
  63. package/css/style-delta.css +464 -0
  64. package/css/style-delta.css.map +1 -0
  65. package/css/style-gutenberg.css +18 -18
  66. package/css/style-gutenberg.css.map +1 -1
  67. package/css/style-irp-legacy.css +469 -0
  68. package/css/style-irp-legacy.css.map +1 -0
  69. package/css/style-irp.css +96 -103
  70. package/css/style-irp.css.map +1 -1
  71. package/css/style-legacy.css +462 -0
  72. package/css/style-legacy.css.map +1 -0
  73. package/css/style-mcr-legacy.css +469 -0
  74. package/css/style-mcr-legacy.css.map +1 -0
  75. package/css/style-mcr.css +96 -103
  76. package/css/style-mcr.css.map +1 -1
  77. package/css/style-preventionweb-legacy.css +469 -0
  78. package/css/style-preventionweb-legacy.css.map +1 -0
  79. package/css/style-preventionweb.css +96 -103
  80. package/css/style-preventionweb.css.map +1 -1
  81. package/css/style.css +96 -103
  82. package/css/style.css.map +1 -1
  83. package/error-pages/401.html +10 -11
  84. package/error-pages/403.html +11 -12
  85. package/error-pages/404.html +13 -14
  86. package/error-pages/429.html +12 -13
  87. package/error-pages/500.html +10 -11
  88. package/error-pages/502.html +12 -13
  89. package/error-pages/503.html +12 -13
  90. package/error-pages/504.html +10 -11
  91. package/error-pages/5xx.html +10 -11
  92. package/error-pages/challenge.html +12 -13
  93. package/error-pages/managed-challenge.html +12 -13
  94. package/js/tabs.js +427 -81
  95. package/package.json +1 -1
  96. package/scss/Atom/BaseTypography/Blockquote/blockquote.scss +1 -8
  97. package/scss/Atom/BaseTypography/Cite/cite.scss +2 -2
  98. package/scss/Atom/Images/AuthorImage/author-image.scss +4 -4
  99. package/scss/Atom/Images/ImageCaptionCredit/image-caption-credit.scss +24 -28
  100. package/scss/Atom/Images/ImageCredit/image-credit.scss +1 -1
  101. package/scss/Atom/Layout/Container/container.scss +2 -2
  102. package/scss/Atom/Layout/Grid/grid.scss +1 -1
  103. package/scss/Atom/ReachElement/Details/details.scss +6 -6
  104. package/scss/Atom/ReachElement/Figcaption/figcaption.scss +1 -1
  105. package/scss/Atom/Table/table.scss +0 -8
  106. package/scss/Components/Boilerplate/boilerplate.scss +2 -2
  107. package/scss/Components/Breadcrumbs/breadcrumbs.scss +2 -9
  108. package/scss/Components/Buttons/Chips/chips.scss +5 -19
  109. package/scss/Components/Buttons/CtaButton/buttons.scss +3 -124
  110. package/scss/Components/Buttons/CtaButton/cta-button.scss +124 -0
  111. package/scss/Components/Buttons/ShareButtons/share-buttons.scss +2 -2
  112. package/scss/Components/Cards/Card/card.scss +39 -0
  113. package/scss/Components/ErrorPages/error-pages.scss +12 -12
  114. package/scss/Components/Footer/footer.scss +72 -4
  115. package/scss/Components/Forms/Select/select.scss +2 -2
  116. package/scss/Components/Forms/_form-base.scss +5 -5
  117. package/scss/Components/Forms/_form-legacy.scss +1 -1
  118. package/scss/Components/Gallery/gallery.scss +4 -4
  119. package/scss/Components/Hero/hero.scss +18 -17
  120. package/scss/Components/HighlightBox/highlight-box.scss +5 -5
  121. package/scss/Components/MegaMenu/mega-menu.scss +750 -0
  122. package/scss/Components/MegaMenu/megamenu.scss +3 -733
  123. package/scss/Components/PageHeader/page-header.scss +4 -4
  124. package/scss/Components/Pagination/pagination.scss +2 -2
  125. package/scss/Components/SyndicationSearchWidget/SyndicationSearchWidget.scss +3 -1480
  126. package/scss/Components/SyndicationSearchWidget/syndication-search-widget.scss +1515 -0
  127. package/scss/Components/Tab/tab.scss +66 -7
  128. package/scss/Components/TextCta/text-cta.scss +129 -0
  129. package/scss/Components/TextCta/textcta.scss +3 -27
  130. package/scss/Molecules/ImageCaption/image-caption.scss +6 -16
  131. package/scss/Molecules/SectionHeader/section-header.scss +8 -0
  132. package/scss/Molecules/SectionHeader/sectionheader.scss +3 -8
  133. package/scss/Utilities/FullWidth/FullWidth.scss +3 -23
  134. package/scss/Utilities/FullWidth/full-width.scss +23 -0
  135. package/scss/Utilities/Loader/loader.scss +1 -1
  136. package/scss/Utilities/ShowMore/ShowMore.scss +3 -26
  137. package/scss/Utilities/ShowMore/show-more.scss +26 -0
  138. package/scss/assets/scss/_components.scss +14 -9
  139. package/scss/assets/scss/_foundational.scss +13 -7
  140. package/scss/assets/scss/_mixins.scss +9 -314
  141. package/scss/assets/scss/_utility.scss +19 -71
  142. package/scss/assets/scss/_variables-delta.scss +105 -0
  143. package/scss/assets/scss/_variables-irp.scss +1 -1
  144. package/scss/assets/scss/_variables-mcr.scss +1 -1
  145. package/scss/assets/scss/_variables-preventionweb.scss +1 -1
  146. package/scss/assets/scss/_variables.scss +68 -34
  147. package/scss/assets/scss/style-delta.scss +8 -0
  148. package/scss/assets/scss/style-gutenberg.scss +2 -2
  149. package/scss/assets/scss/style-irp-legacy.scss +20 -0
  150. package/scss/assets/scss/style-legacy.scss +20 -0
  151. package/scss/assets/scss/style-mcr-legacy.scss +20 -0
  152. package/scss/assets/scss/style-preventionweb-legacy.scss +20 -0
  153. package/scss/Components/BlockquoteComponent/blockquotecomp.scss +0 -31
  154. package/scss/Components/Buttons/CtaLink/cta-link.scss +0 -61
  155. /package/scss/Components/TableOfContents/{TableOfContents.scss → table-of-contents.scss} +0 -0
package/js/tabs.js CHANGED
@@ -1,4 +1,71 @@
1
1
  // mg-tabs
2
+
3
+ // Matches $mg-breakpoint-mobile in _variables.scss
4
+ const BREAKPOINT_MOBILE = 480;
5
+
6
+ /**
7
+ * Determine whether a tab container should behave as stacked (disclosure)
8
+ * rather than horizontal tabs.
9
+ * @param {Element} container - the [data-mg-js-tabs] element
10
+ * @returns {boolean}
11
+ */
12
+ function isStacked(container) {
13
+ return (
14
+ container.dataset.mgJsTabsVariant === 'stacked' ||
15
+ window.innerWidth < BREAKPOINT_MOBILE
16
+ );
17
+ }
18
+
19
+ /**
20
+ * Set the open/close state of a stacked disclosure panel.
21
+ * @param {Element} trigger - the tab link acting as disclosure trigger
22
+ * @param {Element} panel - the section panel
23
+ * @param {boolean} open - true to open, false to close
24
+ */
25
+ export function setDisclosureState(trigger, panel, open) {
26
+ if (open) {
27
+ panel.removeAttribute('hidden');
28
+ trigger.setAttribute('aria-expanded', 'true');
29
+ trigger.classList.add('is-active', 'mg-tabs__stacked--open');
30
+ } else {
31
+ panel.setAttribute('hidden', 'until-found');
32
+ trigger.setAttribute('aria-expanded', 'false');
33
+ trigger.classList.remove('is-active', 'mg-tabs__stacked--open');
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Find the next visible tab index, skipping items hidden by filtering.
39
+ * @param {NodeList} tabs - all tab trigger elements
40
+ * @param {number} fromIndex - current index to start searching from
41
+ * @param {number} step - direction: +1 for forward, -1 for backward
42
+ * @returns {number} index of the next visible tab, or -1 if all hidden
43
+ */
44
+ function findVisibleTab(tabs, fromIndex, step) {
45
+ const len = tabs.length;
46
+ let idx = fromIndex;
47
+ for (let i = 0; i < len; i++) {
48
+ idx = (idx + step + len) % len;
49
+ if (!tabs[idx].closest('.mg-tabs__item--hidden')) return idx;
50
+ }
51
+ return -1;
52
+ }
53
+
54
+ /**
55
+ * Normalize text for filter matching: collapse smart quotes, dashes,
56
+ * and other typographic punctuation to their plain ASCII equivalents.
57
+ * @param {string} text
58
+ * @returns {string}
59
+ */
60
+ function normalizeText(text) {
61
+ return text
62
+ .replace(/[\u2018\u2019\u201A\u2032]/g, "'") // smart single quotes, prime
63
+ .replace(/[\u201C\u201D\u201E\u2033]/g, '"') // smart double quotes, double prime
64
+ .replace(/[\u2013\u2014\u2015]/g, '-') // en dash, em dash, horizontal bar
65
+ .replace(/\u2026/g, '...') // ellipsis
66
+ .replace(/[\u00A0]/g, ' '); // non-breaking space
67
+ }
68
+
2
69
  /**
3
70
  * Initialize tabs on a page
4
71
  * @param {boolean} [activateDeepLinkOnLoad] - if deep linked tabs should be activated on page load, defaults to true
@@ -19,7 +86,7 @@ export function mgTabs(scope, activateDeepLinkOnLoad = true) {
19
86
  */
20
87
  export function mgTabsRuntime(scope, activateDeepLinkOnLoad) {
21
88
  var scope = scope || document;
22
- var activateDeepLinkOnLoad = activateDeepLinkOnLoad || true;
89
+ var activateDeepLinkOnLoad = activateDeepLinkOnLoad ?? true;
23
90
 
24
91
  // Get relevant elements and collections
25
92
  if (scope.hasAttribute('data-mg-js-tabs')) {
@@ -29,7 +96,7 @@ export function mgTabsRuntime(scope, activateDeepLinkOnLoad) {
29
96
  scope.querySelectorAll('[data-mg-js-tabs]') || newTab.closest('.mg-tabs'); // compatibility with v1 tabs
30
97
  }
31
98
  const tabs = scope.querySelectorAll('.mg-tabs__link');
32
- var panels = scope.querySelectorAll('[id^="mg-tabs__section"]');
99
+ var panels = scope.querySelectorAll('[id^="mg-tabs__section"]:not(a)');
33
100
  // v1 compatibility
34
101
  // If panels is empty, try finding them in data-mg-js-tabs-content
35
102
  if (!panels.length) {
@@ -37,7 +104,7 @@ export function mgTabsRuntime(scope, activateDeepLinkOnLoad) {
37
104
  .closest('.mg-tabs')
38
105
  .querySelector('[data-mg-js-tabs-content]');
39
106
  if (tabContent) {
40
- panels = tabContent.querySelectorAll('[id^="mg-tabs__section"]');
107
+ panels = tabContent.querySelectorAll('[id^="mg-tabs__section"]:not(a)');
41
108
  }
42
109
  }
43
110
 
@@ -50,24 +117,54 @@ export function mgTabsRuntime(scope, activateDeepLinkOnLoad) {
50
117
  return;
51
118
  }
52
119
 
120
+ // Normalize tabsList to an array so we can iterate uniformly
121
+ // (when scope has [data-mg-js-tabs], tabsList is a single Element)
122
+ const tabsListArray = tabsList.nodeType
123
+ ? [tabsList]
124
+ : Array.from(tabsList);
125
+
53
126
  // Check if tabs have already been initialized
54
- if (tabsList.hasAttribute('data-mg-tabs-initialized')) {
127
+ if (tabsList.hasAttribute && tabsList.hasAttribute('data-mg-tabs-initialized')) {
55
128
  return;
56
129
  }
57
- tabsList.setAttribute('data-mg-tabs-initialized', 'true');
130
+ if (tabsList.hasAttribute) {
131
+ tabsList.setAttribute('data-mg-tabs-initialized', 'true');
132
+ } else if (tabsListArray.length > 0) {
133
+ if (tabsListArray[0].hasAttribute('data-mg-tabs-initialized')) {
134
+ return;
135
+ }
136
+ tabsListArray[0].setAttribute('data-mg-tabs-initialized', 'true');
137
+ }
138
+
139
+ // Determine variant from the container element
140
+ const container = tabsListArray[0];
141
+ const stacked = isStacked(container);
58
142
 
59
- // Add semantics are remove user focusability for each tab
143
+ // Add semantics and focusability for each tab
60
144
  Array.prototype.forEach.call(tabs, (tab, i) => {
61
- const tabId = tab.href.split('#')[1]; // calculate an ID based off the tab href (todo: add support for a data-vf-js-tab-id, and if set use that)
62
- tab.setAttribute('role', 'tab');
63
- tab.setAttribute('id', tabId);
64
- tab.setAttribute('data-tabs__item', tabId);
65
- tab.setAttribute('tabindex', '-1');
66
- tab.parentNode.setAttribute('role', 'presentation');
145
+ const panelId = tab.href.split('#')[1];
146
+ tab.setAttribute('data-tabs__item', panelId);
147
+
148
+ // Give trigger a distinct ID so it doesn't collide with the panel's ID
149
+ tab.setAttribute('id', panelId + '--trigger');
150
+
151
+ if (stacked) {
152
+ // Disclosure pattern: each trigger is a button that toggles its panel
153
+ tab.setAttribute('role', 'button');
154
+ tab.setAttribute('aria-expanded', 'false');
155
+ tab.setAttribute('aria-controls', panelId);
156
+ tab.parentNode.removeAttribute('role');
157
+ // Stacked triggers must be in the Tab order (disclosure buttons)
158
+ tab.removeAttribute('tabindex');
159
+ } else {
160
+ // Horizontal tabs: standard tablist pattern (roving tabindex)
161
+ tab.setAttribute('role', 'tab');
162
+ tab.parentNode.setAttribute('role', 'presentation');
163
+ tab.setAttribute('tabindex', '-1');
164
+ }
67
165
 
68
166
  // Reset any active tabs from a previous JS call
69
167
  tab.removeAttribute('aria-selected');
70
- tab.setAttribute('tabindex', '-1');
71
168
  tab.classList.remove('is-active');
72
169
 
73
170
  // Handle clicking of tabs for mouse users
@@ -79,47 +176,86 @@ export function mgTabsRuntime(scope, activateDeepLinkOnLoad) {
79
176
  // Handle keydown events for keyboard users
80
177
  tab.addEventListener('keydown', e => {
81
178
  // Get the index of the current tab in the tabs node list
82
- let index = Array.prototype.indexOf.call(tabs, e.currentTarget);
83
- // Work out which key the user is pressing and
84
- // Calculate the new tab's index where appropriate
85
- let dir =
86
- e.which === 37
87
- ? index - 1
88
- : e.which === 39
89
- ? index + 1
90
- : e.which === 40
91
- ? 'down'
92
- : null;
93
- if (dir !== null) {
179
+ const index = Array.prototype.indexOf.call(tabs, e.currentTarget);
180
+ const parentContainer =
181
+ e.currentTarget.closest('[data-mg-js-tabs]') ||
182
+ e.currentTarget.closest('.mg-tabs');
183
+ const currentlyStacked = isStacked(parentContainer);
184
+
185
+ // Stacked: Space/Enter to toggle, Up/Down to navigate, Home/End for first/last
186
+ // Horizontal: Left/Right to navigate tabs, Down to focus panel
187
+ if (currentlyStacked && (e.key === ' ' || e.key === 'Enter')) {
94
188
  e.preventDefault();
95
- // If the down key is pressed, move focus to the open panel,
96
- // otherwise switch to the adjacent tab
97
- dir === 'down'
98
- ? panels[i].focus({ preventScroll: true })
99
- : tabs[dir]
100
- ? mgTabsSwitch(tabs[dir], panels)
101
- : void 0;
189
+ mgTabsSwitch(e.currentTarget, panels);
190
+ return;
191
+ }
192
+
193
+ const prevKey = currentlyStacked ? 'ArrowUp' : 'ArrowLeft';
194
+ const nextKey = currentlyStacked ? 'ArrowDown' : 'ArrowRight';
195
+ let dir = null;
196
+
197
+ if (e.key === prevKey) {
198
+ dir = findVisibleTab(tabs, index, -1);
199
+ } else if (e.key === nextKey) {
200
+ dir = findVisibleTab(tabs, index, 1);
201
+ } else if (!currentlyStacked && e.key === 'ArrowDown') {
202
+ dir = 'down';
203
+ } else if (e.key === 'Home') {
204
+ dir = findVisibleTab(tabs, -1, 1);
205
+ } else if (e.key === 'End') {
206
+ dir = findVisibleTab(tabs, tabs.length, -1);
207
+ }
208
+
209
+ if (dir !== null && dir !== -1) {
210
+ e.preventDefault();
211
+ if (dir === 'down') {
212
+ panels[i].focus({ preventScroll: true });
213
+ } else if (tabs[dir]) {
214
+ if (currentlyStacked) {
215
+ // In stacked mode, just move focus without switching
216
+ tabs[dir].focus({ preventScroll: true });
217
+ } else {
218
+ mgTabsSwitch(tabs[dir], panels);
219
+ }
220
+ }
102
221
  }
103
222
  });
104
223
  });
105
224
 
106
- // Add tab panel semantics and hide them all
225
+ // Add panel semantics and hide them all
107
226
  Array.prototype.forEach.call(panels, panel => {
108
- panel.setAttribute('role', 'tabpanel');
227
+ const panelId = panel.id;
228
+ // Find the corresponding tab trigger
229
+ const correspondingTab = scope.querySelector(`[data-tabs__item="${panelId}"]`);
230
+ const labelId = correspondingTab ? correspondingTab.id : panelId;
231
+
232
+ if (stacked) {
233
+ // Disclosure pattern: panels are regions labelled by their trigger
234
+ panel.setAttribute('role', 'region');
235
+ panel.setAttribute('aria-labelledby', labelId);
236
+ panel.setAttribute('hidden', 'until-found');
237
+ } else {
238
+ // Horizontal tabs: standard tabpanel
239
+ panel.setAttribute('role', 'tabpanel');
240
+ panel.setAttribute('aria-labelledby', labelId);
241
+ panel.hidden = true;
242
+ }
109
243
  panel.setAttribute('tabindex', '-1');
110
- // let id = panel.getAttribute("id");
111
- panel.setAttribute('aria-labelledby', panel.id);
112
- panel.hidden = true;
113
244
  });
114
245
 
115
- // Add the tabsList role to the first <ul> in the .tabbed container
116
- Array.prototype.forEach.call(tabsList, tabsListset => {
117
- tabsListset.setAttribute('role', 'tablist');
118
- // Show the first tab (if the parent tabset is not stacked and screen is not narrow)
119
- if (
120
- tabsListset.dataset.mgJsTabsVariant != 'stacked' &&
121
- window.innerWidth >= 600
122
- ) {
246
+ // Set up container roles and initial state
247
+ tabsListArray.forEach(tabsListset => {
248
+ if (!stacked) {
249
+ // Apply role="tablist" to the <ul> so role="tab" children are valid
250
+ const tabListEl = tabsListset.querySelector('.mg-tabs__list') || tabsListset;
251
+ tabListEl.setAttribute('role', 'tablist');
252
+
253
+ // All direct <li> children of the tablist need role="presentation"
254
+ // so they don't break the tablist → tab hierarchy
255
+ tabListEl.querySelectorAll(':scope > li').forEach(li => {
256
+ li.setAttribute('role', 'presentation');
257
+ });
258
+
123
259
  // Initially activate the first tab
124
260
  let firstTab = tabsListset.querySelectorAll('.mg-tabs__link')[0];
125
261
  firstTab.removeAttribute('tabindex');
@@ -142,6 +278,11 @@ export function mgTabsRuntime(scope, activateDeepLinkOnLoad) {
142
278
  mgTabsDeepLinkOnLoad(tabs, panels);
143
279
  }
144
280
 
281
+ // Initialize filter if the container has the filterable attribute
282
+ if (stacked && container.dataset.mgJsTabsFilterable != null) {
283
+ mgTabsInitFilter(container, tabs, panels);
284
+ }
285
+
145
286
  // When using anchor links after load, activate the corresponding tab
146
287
  window.addEventListener('hashchange', () => {
147
288
  const hash = window.location.hash
@@ -169,36 +310,56 @@ const mgTabsSwitch = (newTab, panels) => {
169
310
  // get the parent ul of the clicked tab
170
311
  let parentTabContainer =
171
312
  newTab.closest('[data-mg-js-tabs]') || newTab.closest('.mg-tabs'); // compatibility with v1 tabs
313
+ const stacked = isStacked(parentTabContainer);
314
+ const targetPanelId = newTab.getAttribute('data-tabs__item');
172
315
  let oldTab = parentTabContainer.querySelector('[aria-selected]');
173
- const behaveAsHorizontalTabs =
174
- parentTabContainer.dataset.mgJsTabsVariant != 'stacked' &&
175
- window.innerWidth >= 600;
176
-
177
- // if it is marked as stacked or small screen (this is not an inverse of behaveAsHorizontalTabs)
178
- if (
179
- parentTabContainer.dataset.mgJsTabsVariant == 'stacked' ||
180
- window.innerWidth <= 600
181
- ) {
316
+
317
+ // if stacked, toggle the clicked panel independently
318
+ if (stacked) {
319
+ const isSingleOpen = parentTabContainer.dataset.mgJsTabsSingleOpen != null;
320
+
182
321
  for (let item = 0; item < panels.length; item++) {
183
322
  const panel = panels[item];
184
- if (panel.id === newTab.id) {
185
- panel.hidden = !panel.hidden;
323
+ if (panel.id === targetPanelId) {
324
+ const wasHidden = panel.hidden || panel.getAttribute('hidden') === 'until-found';
325
+
326
+ // In single-open mode, close all other panels before opening
327
+ if (isSingleOpen && wasHidden) {
328
+ for (let j = 0; j < panels.length; j++) {
329
+ const otherPanel = panels[j];
330
+ if (otherPanel !== panel && !otherPanel.hidden && otherPanel.getAttribute('hidden') !== 'until-found') {
331
+ const otherTrigger = parentTabContainer.querySelector(
332
+ `[data-tabs__item="${otherPanel.id}"]`
333
+ );
334
+ if (otherTrigger) {
335
+ setDisclosureState(otherTrigger, otherPanel, false);
336
+ }
337
+ }
338
+ }
339
+ }
340
+
341
+ setDisclosureState(newTab, panel, wasHidden);
342
+ break;
186
343
  }
187
344
  }
345
+ // In stacked/disclosure mode, we don't deselect other tabs or use aria-selected
346
+ newTab.focus({ preventScroll: true });
347
+ return;
188
348
  }
189
349
 
350
+ // --- Horizontal tab behavior below ---
351
+
190
352
  if (oldTab) {
191
353
  oldTab.removeAttribute('aria-selected');
192
354
  oldTab.setAttribute('tabindex', '-1');
193
355
  oldTab.classList.remove('is-active');
194
356
 
195
- if (behaveAsHorizontalTabs) {
196
- // normal horizontal tabs
197
- for (let item = 0; item < panels.length; item++) {
198
- const panel = panels[item];
199
- if (panel.id === oldTab.id) {
200
- panel.hidden = true;
201
- }
357
+ const oldPanelId = oldTab.getAttribute('data-tabs__item');
358
+ for (let item = 0; item < panels.length; item++) {
359
+ const panel = panels[item];
360
+ if (panel.id === oldPanelId) {
361
+ panel.hidden = true;
362
+ break;
202
363
  }
203
364
  }
204
365
  }
@@ -209,17 +370,194 @@ const mgTabsSwitch = (newTab, panels) => {
209
370
  // Set the selected state
210
371
  newTab.setAttribute('aria-selected', 'true');
211
372
  newTab.classList.add('is-active');
212
- newTab.classList.toggle('mg-tabs__stacked--open'); // we always track the user's open state, even if not stacked as we may switch to mobile view // Get the indices of the new tab to find the correct tab panel to show
213
- if (behaveAsHorizontalTabs) {
214
- for (let item = 0; item < panels.length; item++) {
215
- const panel = panels[item];
216
- if (panel.id === newTab.id) {
217
- panel.hidden = false;
373
+ newTab.classList.add('mg-tabs__stacked--open'); // track open state for potential mobile view switch
374
+
375
+ for (let item = 0; item < panels.length; item++) {
376
+ const panel = panels[item];
377
+ if (panel.id === targetPanelId) {
378
+ panel.hidden = false;
379
+ break;
380
+ }
381
+ }
382
+ };
383
+
384
+ /**
385
+ * Apply default open/close state for stacked tabs.
386
+ *
387
+ * Priority: per-item `data-mg-js-tabs-default` > container `data-mg-js-tabs-default-open` > open first.
388
+ *
389
+ * @param {Element} container - the [data-mg-js-tabs] element
390
+ * @param {NodeList|Array} tabs - the trigger links
391
+ * @param {NodeList|Array} panels - the section panels
392
+ */
393
+ export function mgTabsApplyStackedDefaults(container, tabs, panels) {
394
+ const defaultOpen = container.dataset.mgJsTabsDefaultOpen;
395
+
396
+ Array.prototype.forEach.call(tabs, (tab, i) => {
397
+ const tabId = tab.getAttribute('data-tabs__item');
398
+ let matchingPanel = null;
399
+ for (let j = 0; j < panels.length; j++) {
400
+ if (panels[j].id === tabId) {
401
+ matchingPanel = panels[j];
218
402
  break;
219
403
  }
220
404
  }
405
+ if (!matchingPanel) return;
406
+
407
+ const perItemDefault = tab.getAttribute('data-mg-js-tabs-default');
408
+ let shouldOpen;
409
+
410
+ if (perItemDefault === 'true') {
411
+ shouldOpen = true;
412
+ } else if (perItemDefault === 'false') {
413
+ shouldOpen = false;
414
+ } else if (defaultOpen === 'true') {
415
+ shouldOpen = true;
416
+ } else if (defaultOpen === 'false') {
417
+ shouldOpen = false;
418
+ } else {
419
+ // No container default — preserve existing behavior: open first tab
420
+ shouldOpen = (i === 0);
421
+ }
422
+
423
+ setDisclosureState(tab, matchingPanel, shouldOpen);
424
+ });
425
+ }
426
+
427
+ /**
428
+ * Initialize filter input for stacked tabs.
429
+ * Injects a search input, sr-only hint, and status region before the tab list.
430
+ * Handles debounced filtering, show/hide via CSS classes, panel expand/collapse,
431
+ * focus rescue, and live region announcements.
432
+ *
433
+ * @param {Element} container - the [data-mg-js-tabs] element
434
+ * @param {NodeList} tabs - the trigger links
435
+ * @param {NodeList} panels - the section panels
436
+ */
437
+ function mgTabsInitFilter(container, tabs, panels) {
438
+ const placeholder = container.dataset.mgJsTabsFilterPlaceholder || 'Filter sections\u2026';
439
+ const ariaLabel = placeholder.replace(/\u2026$/, '').trim();
440
+ const totalCount = tabs.length;
441
+
442
+ // Build filter DOM
443
+ const filterWrapper = document.createElement('div');
444
+ filterWrapper.className = 'mg-tabs__filter';
445
+
446
+ const input = document.createElement('input');
447
+ input.type = 'search';
448
+ input.className = 'mg-form-input mg-tabs__filter-input';
449
+ input.placeholder = placeholder;
450
+ input.setAttribute('aria-label', ariaLabel);
451
+
452
+ const hintId = 'mg-tabs-filter-hint-' + Math.random().toString(36).slice(2, 8);
453
+ input.setAttribute('aria-describedby', hintId);
454
+
455
+ const hint = document.createElement('span');
456
+ hint.id = hintId;
457
+ hint.className = 'mg-u-sr-only';
458
+ hint.textContent = 'Results will filter as you type';
459
+
460
+ filterWrapper.appendChild(input);
461
+ filterWrapper.appendChild(hint);
462
+
463
+ // Status region for screen reader announcements
464
+ const status = document.createElement('p');
465
+ status.className = 'mg-u-sr-only';
466
+ status.setAttribute('role', 'status');
467
+
468
+ // Insert before the tab list
469
+ const tabList = container.querySelector('.mg-tabs__list');
470
+ container.insertBefore(filterWrapper, tabList);
471
+
472
+ // No-results element (hidden by default, shown when matchCount === 0)
473
+ // No role="status" here — the sr-only status element handles announcements
474
+ const noResults = document.createElement('p');
475
+ noResults.className = 'mg-tabs__no-results mg-tabs__no-results--hidden';
476
+ noResults.textContent = 'No matching sections found.';
477
+ if (tabList.nextSibling) {
478
+ container.insertBefore(noResults, tabList.nextSibling);
479
+ } else {
480
+ container.appendChild(noResults);
221
481
  }
222
- };
482
+ container.appendChild(status);
483
+
484
+ let hasFiltered = false;
485
+ let debounceTimer = null;
486
+
487
+ function applyFilter() {
488
+ const query = input.value.toLowerCase().trim();
489
+
490
+ if (!query) {
491
+ if (!hasFiltered) return;
492
+ // Restore: show all items, reset to default state
493
+ const items = container.querySelectorAll('.mg-tabs__item');
494
+ items.forEach(item => {
495
+ item.classList.remove('mg-tabs__item--hidden');
496
+ const contentLi = item.nextElementSibling;
497
+ if (contentLi?.classList.contains('mg-tabs-content')) {
498
+ contentLi.classList.remove('mg-tabs-content--hidden');
499
+ }
500
+ });
501
+ mgTabsApplyStackedDefaults(container, tabs, panels);
502
+ noResults.classList.add('mg-tabs__no-results--hidden');
503
+ status.textContent = '';
504
+ return;
505
+ }
506
+
507
+ hasFiltered = true;
508
+ const items = container.querySelectorAll('.mg-tabs__item');
509
+ const words = normalizeText(query).split(/\s+/).filter(Boolean);
510
+ if (words.length === 0) return;
511
+ let matches = 0;
512
+
513
+ items.forEach(item => {
514
+ const trigger = item.querySelector('.mg-tabs__link');
515
+ const contentLi = item.nextElementSibling;
516
+ const panel = contentLi?.querySelector('.mg-tabs__section');
517
+
518
+ const triggerText = trigger?.textContent?.toLowerCase() || '';
519
+ const panelText = panel?.textContent?.toLowerCase() || '';
520
+ const combinedText = normalizeText(triggerText + ' ' + panelText);
521
+ const isMatch = words.every(word => combinedText.includes(word));
522
+
523
+ item.classList.toggle('mg-tabs__item--hidden', !isMatch);
524
+ if (contentLi?.classList.contains('mg-tabs-content')) {
525
+ contentLi.classList.toggle('mg-tabs-content--hidden', !isMatch);
526
+ }
527
+
528
+ if (panel && trigger) {
529
+ setDisclosureState(trigger, panel, isMatch);
530
+ }
531
+ if (isMatch) matches++;
532
+ });
533
+
534
+ // Focus rescue: if active element is now hidden, move to filter input
535
+ const active = document.activeElement;
536
+ if (active && active.closest && active.closest('.mg-tabs__item--hidden')) {
537
+ input.focus();
538
+ }
539
+
540
+ // Update live regions
541
+ if (matches === 0) {
542
+ noResults.classList.remove('mg-tabs__no-results--hidden');
543
+ status.textContent = 'No matching sections found.';
544
+ } else {
545
+ noResults.classList.add('mg-tabs__no-results--hidden');
546
+ status.textContent = matches + ' of ' + totalCount + ' sections match.';
547
+ }
548
+ }
549
+
550
+ input.addEventListener('input', () => {
551
+ clearTimeout(debounceTimer);
552
+ debounceTimer = setTimeout(applyFilter, 150);
553
+ });
554
+
555
+ // Also handle the native search clear button (fires 'search' event in some browsers)
556
+ input.addEventListener('search', () => {
557
+ clearTimeout(debounceTimer);
558
+ applyFilter();
559
+ });
560
+ }
223
561
 
224
562
  function mgTabsDeepLinkOnLoad(tabs, panels) {
225
563
  var mgTabAnchorFound = false;
@@ -241,18 +579,26 @@ function mgTabsDeepLinkOnLoad(tabs, panels) {
241
579
  }
242
580
 
243
581
  if (!mgTabAnchorFound) {
244
- // No hash found - look for default tab
245
- let defaultTabFound = false;
246
- Array.from(tabs).forEach(tab => {
247
- if (tab.getAttribute('data-mg-js-tabs-default') === 'true') {
248
- mgTabsSwitch(tab, panels);
249
- defaultTabFound = true;
250
- }
251
- });
582
+ // Determine the container to check for stacked mode
583
+ const container = tabs.length > 0
584
+ ? (tabs[0].closest('[data-mg-js-tabs]') || tabs[0].closest('.mg-tabs'))
585
+ : null;
252
586
 
253
- // If no default tab is found, activate the first tab
254
- if (!defaultTabFound && tabs.length > 0) {
255
- mgTabsSwitch(tabs[0], panels);
587
+ if (container && isStacked(container)) {
588
+ mgTabsApplyStackedDefaults(container, tabs, panels);
589
+ } else {
590
+ // Horizontal tabs: find default or activate first
591
+ let defaultTabFound = false;
592
+ Array.from(tabs).forEach(tab => {
593
+ if (tab.getAttribute('data-mg-js-tabs-default') === 'true') {
594
+ mgTabsSwitch(tab, panels);
595
+ defaultTabFound = true;
596
+ }
597
+ });
598
+
599
+ if (!defaultTabFound && tabs.length > 0) {
600
+ mgTabsSwitch(tabs[0], panels);
601
+ }
256
602
  }
257
603
  }
258
604
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@undrr/undrr-mangrove",
3
- "version": "1.3.3",
3
+ "version": "1.4.0",
4
4
  "description": "Mangrove design System for UNDRR",
5
5
  "main": "dist/index.js",
6
6
  "files": [
@@ -1,4 +1,4 @@
1
- blockquote {
1
+ .mg-body blockquote {
2
2
  font-size: $mg-font-size-600;
3
3
  font-weight: 600;
4
4
  line-height: 1.16;
@@ -9,10 +9,3 @@ blockquote {
9
9
  line-height: 1.15;
10
10
  }
11
11
  }
12
-
13
- // burmese lang
14
- :lang(my) {
15
- blockquote {
16
- line-height: 1.7;
17
- }
18
- }
@@ -1,6 +1,6 @@
1
- cite {
1
+ .mg-body cite {
2
2
  display: block;
3
3
  font-style: inherit;
4
4
  font-weight: 400;
5
- margin-top: 1.125rem;
5
+ margin-top: mg-rem(11.25);
6
6
  }
@@ -1,6 +1,6 @@
1
1
  /* author image */
2
2
 
3
- .author__img {
3
+ .mg-author-image {
4
4
  border-radius: $author-image-radius;
5
5
  height: 80px;
6
6
  min-width: 80px;
@@ -32,7 +32,7 @@
32
32
  }
33
33
 
34
34
  @each $name, $color in $colors {
35
- &.#{$name}::before {
35
+ &.mg-author-image--#{$name}::before {
36
36
  @include background-image(45deg, $mg-color-yellow, transparent 53%);
37
37
  }
38
38
  }
@@ -42,14 +42,14 @@
42
42
  @extend %img-cover;
43
43
  }
44
44
 
45
- &.large {
45
+ &.mg-author-image--large {
46
46
  height: 180px;
47
47
  width: 180px;
48
48
  }
49
49
  }
50
50
 
51
51
  [dir="rtl"] {
52
- .author__img {
52
+ .mg-author-image {
53
53
  &::before {
54
54
  @include transform(scaleX(-1));
55
55
  }