@undrr/undrr-mangrove 2.0.0-rc.2 → 2.0.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 (124) hide show
  1. package/README.md +11 -6
  2. package/components/BookCard.js +1 -1
  3. package/components/Breadcrumbs.js +2 -2
  4. package/components/Checkbox.js +1 -1
  5. package/components/Chips.js +1 -1
  6. package/components/CopyButton.js +2 -2
  7. package/components/CtaButton.js +1 -1
  8. package/components/Drawer.js +1 -1
  9. package/components/EmbedContainer.js +1 -1
  10. package/components/Footer.js +1 -1
  11. package/components/FormAction.js +1 -1
  12. package/components/FormErrorSummary.js +1 -1
  13. package/components/FormGroup.js +1 -1
  14. package/components/FullWidth.js +1 -1
  15. package/components/Gallery.js +2 -2
  16. package/components/Hero.js +2 -2
  17. package/components/HighlightBox.js +1 -1
  18. package/components/HorizontalBookCard.js +1 -1
  19. package/components/HorizontalCard.js +1 -1
  20. package/components/IconCard.js +1 -1
  21. package/components/Legend.js +1 -1
  22. package/components/Loader.js +1 -1
  23. package/components/MegaMenu.js +2 -2
  24. package/components/Notice.js +2 -2
  25. package/components/PageHeader.js +2 -2
  26. package/components/Pager.js +2 -2
  27. package/components/QuoteHighlight.js +2 -2
  28. package/components/QuoteHighlight.js.LICENSE.txt +2 -0
  29. package/components/Radio.js +1 -1
  30. package/components/Range.js +1 -1
  31. package/components/ScrollContainer.js +2 -2
  32. package/components/SegmentedControl.js +9 -0
  33. package/components/SegmentedControl.js.LICENSE.txt +9 -0
  34. package/components/Select.js +1 -1
  35. package/components/ServiceNotice.js +2 -2
  36. package/components/ShareButtons.js +2 -2
  37. package/components/ShowMore.js +2 -2
  38. package/components/SkipLink.js +1 -1
  39. package/components/StatsCard.js +1 -1
  40. package/components/SyndicationSearchWidget.js +2 -2
  41. package/components/Tab.js +2 -2
  42. package/components/TextCta.js +1 -1
  43. package/components/TextInput.js +1 -1
  44. package/components/Textarea.js +1 -1
  45. package/components/Tree.js +1 -1
  46. package/components/UserFeedback.js +1 -1
  47. package/components/VerticalCard.js +1 -1
  48. package/components/hydrate.js +2 -2
  49. package/css/style-all.css +60 -40
  50. package/css/style-all.css.map +1 -1
  51. package/css/style-delta.css +59 -39
  52. package/css/style-delta.css.map +1 -1
  53. package/css/style-gutenberg.css +16 -7
  54. package/css/style-gutenberg.css.map +1 -1
  55. package/css/style-irp.css +60 -40
  56. package/css/style-irp.css.map +1 -1
  57. package/css/style-mcr.css +59 -39
  58. package/css/style-mcr.css.map +1 -1
  59. package/css/style-preventionweb.css +60 -40
  60. package/css/style-preventionweb.css.map +1 -1
  61. package/css/style.css +59 -39
  62. package/css/style.css.map +1 -1
  63. package/error-pages/401.html +3 -3
  64. package/error-pages/403.html +3 -3
  65. package/error-pages/404.html +4 -4
  66. package/error-pages/429.html +3 -3
  67. package/error-pages/500.html +6 -6
  68. package/error-pages/502.html +3 -3
  69. package/error-pages/503.html +3 -3
  70. package/error-pages/504.html +3 -3
  71. package/error-pages/5xx.html +4 -4
  72. package/error-pages/challenge.html +3 -3
  73. package/error-pages/managed-challenge.html +3 -3
  74. package/js/README.md +31 -25
  75. package/js/copy-button.js +17 -0
  76. package/js/drawer.js +752 -0
  77. package/js/hub-header.js +217 -0
  78. package/js/show-more.js +144 -10
  79. package/js/switch-pending.js +195 -46
  80. package/package.json +8 -3
  81. package/scss/Atom/BaseTypography/Blockquote/blockquote.scss +2 -1
  82. package/scss/Atom/BaseTypography/Cite/cite.scss +2 -1
  83. package/scss/Atom/BaseTypography/Code/code.scss +8 -2
  84. package/scss/Atom/Icons/icons.scss +1 -1
  85. package/scss/Atom/Images/AuthorImage/author-image.scss +16 -1
  86. package/scss/Atom/StatusLabel/status-label.scss +331 -8
  87. package/scss/Atom/Table/table.scss +8 -3
  88. package/scss/Atom/Tag/tag.scss +20 -2
  89. package/scss/Components/Breadcrumbs/breadcrumbs.scss +8 -4
  90. package/scss/Components/Buttons/CtaButton/cta-button.scss +15 -0
  91. package/scss/Components/Cards/Card/card.scss +37 -10
  92. package/scss/Components/Cards/StatsCard/stats-card.scss +2 -10
  93. package/scss/Components/DataViz/Legend/legend.scss +2 -1
  94. package/scss/Components/ErrorPages/error-pages.scss +8 -0
  95. package/scss/Components/Forms/SegmentedControl/segmented-control.scss +412 -0
  96. package/scss/Components/Forms/_form-base.scss +109 -18
  97. package/scss/Components/Hero/hero.scss +296 -57
  98. package/scss/Components/MegaMenu/mega-menu.scss +26 -6
  99. package/scss/Components/OnThisPageNav/on-this-page-nav.scss +3 -1
  100. package/scss/Components/PageHeader/page-header.scss +13 -28
  101. package/scss/Components/Pager/pager.scss +3 -1
  102. package/scss/Components/QuoteHighlight/quote-highlight.scss +6 -1
  103. package/scss/Components/ScrollContainer/scroll-container.scss +11 -0
  104. package/scss/Components/SyndicationSearchWidget/syndication-search-widget.scss +51 -24
  105. package/scss/Components/Tab/tab.scss +1 -1
  106. package/scss/Components/TextCta/text-cta.scss +11 -3
  107. package/scss/Patterns/HubHeader/hub-header.scss +14 -3
  108. package/scss/Utilities/FullWidth/full-width.scss +14 -1
  109. package/scss/assets/scss/_components.scss +1 -0
  110. package/scss/assets/scss/_fonts.scss +11 -0
  111. package/scss/assets/scss/_foundational.scss +7 -0
  112. package/scss/assets/scss/_mixins.scss +32 -0
  113. package/scss/assets/scss/_tokens-data-viz.scss +9 -0
  114. package/scss/assets/scss/_variables.scss +16 -3
  115. package/scss/assets/scss/generated/_tokens-irp.scss +20 -4
  116. package/scss/assets/scss/generated/_tokens-mangrove.scss +6 -0
  117. package/scss/assets/scss/generated/_tokens-preventionweb.scss +20 -4
  118. package/js/__tests__/copy-button.test.js +0 -112
  119. package/js/__tests__/show-more.test.js +0 -80
  120. package/js/__tests__/table-of-contents.test.js +0 -196
  121. package/scss/Components/SyndicationSearchWidget/SyndicationSearchWidget.scss +0 -11
  122. package/scss/Components/TableOfContents/TableOfContents.scss +0 -11
  123. package/scss/Utilities/FullWidth/FullWidth.scss +0 -11
  124. package/scss/Utilities/ShowMore/ShowMore.scss +0 -11
@@ -0,0 +1,217 @@
1
+ // mg-hub-header
2
+ // Enhances static HubHeader markup: marks the current section from the URL
3
+ // (opt in), keeps the marked section in view and drives the edge fades.
4
+ // See stories/Patterns/ContentHub/ContentHub.mdx.
5
+
6
+ const ROOT_SELECTOR = '[data-mg-js-hub-header]';
7
+ const ANCESTOR_CLASS = 'mg-hub-header__nav-item--ancestor';
8
+
9
+ /**
10
+ * Reduces a link to a path that can be compared with the current page: same
11
+ * origin only, with no query, fragment, trailing slash or `index.html`.
12
+ *
13
+ * @param {string} href - Link href, relative or absolute.
14
+ * @param {string} [base] - Base URL. Defaults to the current location.
15
+ * @returns {string|null} The path, or null for another origin or a bad URL.
16
+ */
17
+ export function hubHeaderPath(href, base = window.location.href) {
18
+ let url;
19
+ try {
20
+ url = new URL(href, base);
21
+ } catch {
22
+ return null;
23
+ }
24
+ if (url.origin !== new URL(base).origin) return null;
25
+ const path = url.pathname.replace(/\/index\.html?$/i, '/');
26
+ return path.length > 1 ? path.replace(/\/+$/, '') || '/' : path;
27
+ }
28
+
29
+ /**
30
+ * Whether the markup already says which section is current. A server or a
31
+ * React consumer that knows better than the URL always wins.
32
+ *
33
+ * @param {HTMLElement} root - The hub header.
34
+ * @returns {boolean} True when any section is already marked.
35
+ */
36
+ function hasExplicitState(root) {
37
+ return Boolean(
38
+ root.querySelector(
39
+ `.mg-hub-header__nav [aria-current], .mg-hub-header__nav [data-hub-current], .${ANCESTOR_CLASS}`
40
+ )
41
+ );
42
+ }
43
+
44
+ /**
45
+ * The path a link points at, or null when it does not point at a page: an
46
+ * empty href, a fragment or a bare query resolves to the current page, so it
47
+ * would match wherever the markup is copied.
48
+ *
49
+ * @param {Element} link - The link.
50
+ * @returns {string|null} The link's path.
51
+ */
52
+ function linkPath(link) {
53
+ const href = (link.getAttribute('href') || '').trim();
54
+ if (!href || href.startsWith('#') || href.startsWith('?')) return null;
55
+ return hubHeaderPath(href);
56
+ }
57
+
58
+ /**
59
+ * Marks the section for the current page. An exact match is the page itself
60
+ * and is announced; otherwise the longest section containing the page is
61
+ * marked as its ancestor, styled the same but not announced.
62
+ *
63
+ * @param {HTMLElement} root - The hub header.
64
+ * @returns {Function} Removes the marks this call added.
65
+ */
66
+ function markCurrent(root) {
67
+ const none = () => {};
68
+ if (hasExplicitState(root)) return none;
69
+ const here = hubHeaderPath(window.location.href);
70
+ const name = root.querySelector('.mg-hub-header__name[href]');
71
+ const home = name ? linkPath(name) : null;
72
+ const links = [...root.querySelectorAll('.mg-hub-header__nav a[href]')];
73
+ const paths = links.map(linkPath);
74
+
75
+ const exact = links.find((link, index) => paths[index] === here);
76
+ if (exact) {
77
+ exact.setAttribute('aria-current', 'page');
78
+ exact.setAttribute('data-hub-current', '');
79
+ return () => {
80
+ exact.removeAttribute('aria-current');
81
+ exact.removeAttribute('data-hub-current');
82
+ };
83
+ }
84
+ if (home && home === here && !name.hasAttribute('aria-current')) {
85
+ name.setAttribute('aria-current', 'page');
86
+ return () => name.removeAttribute('aria-current');
87
+ }
88
+
89
+ // A section that is the hub home, or the site root, contains every page of
90
+ // the hub. As an ancestor it would be marked everywhere, so it can only ever
91
+ // match exactly.
92
+ let best = null;
93
+ links.forEach((link, index) => {
94
+ const path = paths[index];
95
+ if (!path || path === '/' || path === home) return;
96
+ if (!here.startsWith(`${path}/`)) return;
97
+ if (!best || path.length > best.path.length) best = { link, path };
98
+ });
99
+ if (!best) return none;
100
+ const item = best.link.closest('li');
101
+ item?.classList.add(ANCESTOR_CLASS);
102
+ best.link.setAttribute('data-hub-current', '');
103
+ return () => {
104
+ item?.classList.remove(ANCESTOR_CLASS);
105
+ best.link.removeAttribute('data-hub-current');
106
+ };
107
+ }
108
+
109
+ /**
110
+ * Keeps the marked section in view and drives the edge fades, which CSS
111
+ * cannot do on its own because it cannot detect overflow.
112
+ *
113
+ * @param {HTMLElement} root - The hub header.
114
+ * @returns {Function} Removes the listeners and markers this added.
115
+ */
116
+ function enhanceRail(root) {
117
+ const nav = root.querySelector('.mg-hub-header__nav');
118
+ const rail = nav?.querySelector('ul');
119
+ if (!nav || !rail) return () => {};
120
+
121
+ const reveal = () => {
122
+ const selected = rail.querySelector('[data-hub-current]');
123
+ if (!selected || typeof rail.scrollBy !== 'function') return;
124
+ const viewport = rail.getBoundingClientRect();
125
+ const item = selected.getBoundingClientRect();
126
+ let offset = 0;
127
+ if (item.left < viewport.left) offset = item.left - viewport.left;
128
+ else if (item.right > viewport.right) offset = item.right - viewport.right;
129
+ if (offset) rail.scrollBy({ left: offset, behavior: 'instant' });
130
+ };
131
+ const markOverflow = () => {
132
+ const scrolled = Math.abs(rail.scrollLeft);
133
+ nav.toggleAttribute('data-overflow-start', scrolled > 1);
134
+ nav.toggleAttribute(
135
+ 'data-overflow-end',
136
+ scrolled + rail.clientWidth < rail.scrollWidth - 1
137
+ );
138
+ };
139
+ const update = () => {
140
+ reveal();
141
+ markOverflow();
142
+ };
143
+
144
+ update();
145
+ // Only a change to the marked label, such as a web font arriving after first
146
+ // paint, moves the rail back to it. Everything else only recomputes the
147
+ // fades: a resize must not undo the reader's own scrolling, and iOS Safari
148
+ // fires one whenever its toolbar collapses.
149
+ const Observer =
150
+ typeof ResizeObserver === 'undefined' ? null : ResizeObserver;
151
+ const railObserver = Observer ? new Observer(markOverflow) : null;
152
+ railObserver?.observe(nav);
153
+ railObserver?.observe(rail);
154
+ const labelObserver = Observer ? new Observer(update) : null;
155
+ const selected = rail.querySelector('[data-hub-current]');
156
+ if (selected) labelObserver?.observe(selected);
157
+ let active = true;
158
+ document.fonts?.ready.then(() => {
159
+ if (active) update();
160
+ });
161
+ rail.addEventListener('scroll', markOverflow, { passive: true });
162
+ window.addEventListener('resize', markOverflow);
163
+
164
+ return () => {
165
+ active = false;
166
+ railObserver?.disconnect();
167
+ labelObserver?.disconnect();
168
+ rail.removeEventListener('scroll', markOverflow);
169
+ window.removeEventListener('resize', markOverflow);
170
+ nav.removeAttribute('data-overflow-start');
171
+ nav.removeAttribute('data-overflow-end');
172
+ };
173
+ }
174
+
175
+ /**
176
+ * Initializes hub headers. Headers already initialized are skipped, so this
177
+ * can be called again after inserting markup.
178
+ *
179
+ * @param {NodeList|HTMLElement[]|HTMLElement} [scope] - Headers to enhance.
180
+ * Defaults to every [data-mg-js-hub-header] in the document, less any
181
+ * carrying data-mg-hub-header-skip-auto-init.
182
+ * @returns {Function} Removes what this call added, so a header can be
183
+ * initialized again. Useful for React effect cleanup.
184
+ */
185
+ export function mgHubHeader(scope) {
186
+ let roots;
187
+ if (!scope) roots = document.querySelectorAll(ROOT_SELECTOR);
188
+ else if (scope instanceof HTMLElement) roots = [scope];
189
+ else roots = scope;
190
+
191
+ const cleanups = [];
192
+ Array.from(roots).forEach(root => {
193
+ if (!(root instanceof HTMLElement) || !root.matches(ROOT_SELECTOR)) return;
194
+ if (!scope && root.hasAttribute('data-mg-hub-header-skip-auto-init')) {
195
+ return;
196
+ }
197
+ if (root.dataset.mgJsHubHeaderInitialized === 'true') return;
198
+ root.dataset.mgJsHubHeaderInitialized = 'true';
199
+ const unmark = root.hasAttribute('data-mg-hub-header-detect-current')
200
+ ? markCurrent(root)
201
+ : () => {};
202
+ const teardown = enhanceRail(root);
203
+ cleanups.push(() => {
204
+ teardown();
205
+ unmark();
206
+ delete root.dataset.mgJsHubHeaderInitialized;
207
+ });
208
+ });
209
+ return () => cleanups.forEach(cleanup => cleanup());
210
+ }
211
+
212
+ // Auto-wrap so the browser Event object is not passed as scope.
213
+ if (document.readyState === 'loading') {
214
+ document.addEventListener('DOMContentLoaded', () => mgHubHeader(), false);
215
+ } else {
216
+ mgHubHeader();
217
+ }
package/js/show-more.js CHANGED
@@ -1,14 +1,81 @@
1
1
  // mg-show-more
2
2
  // https://gitlab.com/undrr/web-backlog/-/issues/1082
3
3
 
4
+ // Used to give an unlabelled target an id so the toggle can point
5
+ // `aria-controls` at it. The counter lives on `globalThis` rather than in
6
+ // module scope so two loaded copies of this module — a site bundle alongside
7
+ // the CDN build, say — share one sequence instead of each minting
8
+ // `mg-show-more-target-1`. Same hazard, and same fix, as the root counter in
9
+ // `src/hydrate.js` (#1178).
10
+ const TARGET_COUNTER_KEY = Symbol.for('undrr.mangrove.showMore.targetCounter');
11
+
12
+ /**
13
+ * Mints an id that is unused in the current document.
14
+ *
15
+ * @returns {string} A free `mg-show-more-target-N` id.
16
+ */
17
+ function nextTargetId() {
18
+ for (;;) {
19
+ const current = globalThis[TARGET_COUNTER_KEY];
20
+ const next = Number.isSafeInteger(current) && current >= 0 ? current : 0;
21
+ globalThis[TARGET_COUNTER_KEY] = next + 1;
22
+ const id = `mg-show-more-target-${next}`;
23
+ if (!document.getElementById(id)) return id;
24
+ }
25
+ }
26
+
27
+ /**
28
+ * Resolves a toggle's target, nearest scope first.
29
+ *
30
+ * The revealed content need not be a sibling or descendant of its toggle, so
31
+ * there is no container the lookup can be confined to. It can still be
32
+ * *ordered*: climb the toggle's ancestors and return the first match found
33
+ * inside one. For the canonical markup that is the adjacent wrapper, and for
34
+ * two blocks in separate subtrees each toggle finds its own.
35
+ *
36
+ * For a toggle that is in the document the walk ends at `<html>`, an ancestor
37
+ * of every other element, so its last iteration *is* the document-wide lookup:
38
+ * a target anywhere on the page is still found, and it is the same element
39
+ * `document.querySelector` returned whenever nothing nearer matched. The walk
40
+ * is therefore never worse, and better whenever two toggles share a selector.
41
+ * The explicit fallback below is only reached when the toggle is not in the
42
+ * document tree — detached, or inside a shadow root or a `<template>` — where
43
+ * it resolves exactly as it did before.
44
+ *
45
+ * Two toggles under the *same* parent still cannot be told apart: the first
46
+ * scope containing one target contains both, and the earlier one in document
47
+ * order wins. That is also why the React `ShowMore` wrapper mints an id per
48
+ * item rather than relying on this walk. See #1228.
49
+ *
50
+ * @param {HTMLElement} toggle - The toggle whose target to resolve.
51
+ * @param {string} selector - The CSS selector to resolve.
52
+ * @returns {Element|null} The nearest match, or null when nothing matches.
53
+ */
54
+ function resolveTarget(toggle, selector) {
55
+ for (let scope = toggle.parentElement; scope; scope = scope.parentElement) {
56
+ const match = scope.querySelector(selector);
57
+ if (match) return match;
58
+ }
59
+ // `<html>` itself, and anything outside the toggle's ancestor chain (a
60
+ // detached toggle, say), are only reachable from the document.
61
+ return document.querySelector(selector);
62
+ }
63
+
4
64
  /**
5
65
  * Initializes "show more" toggle buttons.
6
66
  *
67
+ * The toggle carries `aria-controls` pointing at the content it collapses.
68
+ * It deliberately does not carry `aria-expanded`: the collapse is visual
69
+ * only. See the accessibility section of `ShowMore.mdx` for why.
70
+ *
7
71
  * @param {NodeList|HTMLElement[]|HTMLElement} [scope] - Elements to init.
8
72
  * Accepts a NodeList, array, or a single HTMLElement.
9
73
  * Defaults to all [data-mg-show-more] in the document.
74
+ * @returns {Function} Removes handlers added by this call and restores each
75
+ * target's initial collapsed state. Useful for React effect cleanup.
10
76
  */
11
77
  export function mgShowMore(scope) {
78
+ const cleanups = [];
12
79
  const mgShowMoreButtons = scope
13
80
  ? scope instanceof HTMLElement
14
81
  ? [scope]
@@ -19,14 +86,9 @@ export function mgShowMore(scope) {
19
86
  // Skip auto-init if the element opts out
20
87
  if (!scope && item.hasAttribute('data-mg-show-more-skip-auto-init')) return;
21
88
  if (item.dataset.mgShowMoreInitialized) return;
22
- item.dataset.mgShowMoreInitialized = 'true';
23
-
24
- item.dataset.dataVfGoogleAnalyticsLabel =
25
- 'Show more: ' + item.dataset.mgShowMoreLabelCollapsed || `Show more`;
26
-
27
89
  const mgShowMoreTargetClass =
28
90
  item.dataset.mgShowMoreTarget || '.mg-show-more--container';
29
- const mgShowMoreTarget = document.querySelector(mgShowMoreTargetClass);
91
+ const mgShowMoreTarget = resolveTarget(item, mgShowMoreTargetClass);
30
92
 
31
93
  if (!mgShowMoreTarget) {
32
94
  console.warn(
@@ -35,7 +97,42 @@ export function mgShowMore(scope) {
35
97
  return;
36
98
  }
37
99
 
38
- item.addEventListener('click', event => {
100
+ item.dataset.mgShowMoreInitialized = 'true';
101
+ const wasCollapsed = mgShowMoreTarget.classList.contains(
102
+ 'mg-show-more--collapsed'
103
+ );
104
+ const wasOpen = item.classList.contains('mg-show-more--button--open');
105
+ const initialText = item.textContent;
106
+ const initialControls = item.getAttribute('aria-controls');
107
+ const initialRole = item.getAttribute('role');
108
+ const onKeydown = event => {
109
+ if (event.key === ' ' || event.key === 'Spacebar') {
110
+ event.preventDefault();
111
+ item.click();
112
+ }
113
+ };
114
+
115
+ item.dataset.dataVfGoogleAnalyticsLabel =
116
+ 'Show more: ' + (item.dataset.mgShowMoreLabelCollapsed || 'Show more');
117
+
118
+ // Relate the toggle to the content it acts on. A <button> already
119
+ // announces as a button; an anchor copied from an earlier version of the
120
+ // docs needs the role.
121
+ if (mgShowMoreTarget.id === '') {
122
+ mgShowMoreTarget.id = nextTargetId();
123
+ }
124
+ item.setAttribute('aria-controls', mgShowMoreTarget.id);
125
+ if (item.tagName !== 'BUTTON') {
126
+ item.setAttribute('role', 'button');
127
+
128
+ // `role="button"` promises Space activates the control, and an anchor
129
+ // does not honour that on its own (WCAG 4.1.2 Name, Role, Value).
130
+ // `preventDefault` keeps Space from scrolling the page.
131
+ item.addEventListener('keydown', onKeydown);
132
+ }
133
+
134
+ const onClick = event => {
135
+ // Also neutralises the dead `href="#"` on a legacy anchor toggle.
39
136
  event.preventDefault();
40
137
  mgShowMoreTarget.classList.toggle('mg-show-more--collapsed');
41
138
 
@@ -51,18 +148,55 @@ export function mgShowMore(scope) {
51
148
  } else {
52
149
  item.classList.add('mg-show-more--button--open');
53
150
  }
54
- });
151
+ };
152
+ item.addEventListener('click', onClick);
153
+
154
+ // The collapse is `max-height` + `overflow: hidden`, which clips content
155
+ // visually but leaves it in the tab order. Without this, Tab lands on a
156
+ // link the user cannot see, inside a box that cannot scroll to reveal it
157
+ // (WCAG 2.4.7 Focus Visible, 2.4.11 Focus Not Obscured). Expanding on
158
+ // entry keeps the focused control visible.
159
+ const onFocusin = event => {
160
+ if (item === event.target || item.contains(event.target)) return;
161
+ if (mgShowMoreTarget.classList.contains('mg-show-more--collapsed')) {
162
+ item.click();
163
+ }
164
+ };
165
+ mgShowMoreTarget.addEventListener('focusin', onFocusin);
55
166
 
56
167
  // Allow items to be shown by clicking anywhere on the collapsed item
57
168
  // https://gitlab.com/undrr/web-backlog/-/issues/1612
58
- mgShowMoreTarget.addEventListener('click', () => {
169
+ const onTargetClick = () => {
59
170
  if (mgShowMoreTarget.classList.contains('mg-show-more--collapsed')) {
60
171
  item.click();
61
172
  }
62
- });
173
+ };
174
+ mgShowMoreTarget.addEventListener('click', onTargetClick);
63
175
 
64
176
  item.click();
177
+ cleanups.push(() => {
178
+ item.removeEventListener('click', onClick);
179
+ item.removeEventListener('keydown', onKeydown);
180
+ mgShowMoreTarget.removeEventListener('focusin', onFocusin);
181
+ mgShowMoreTarget.removeEventListener('click', onTargetClick);
182
+ mgShowMoreTarget.classList.toggle(
183
+ 'mg-show-more--collapsed',
184
+ wasCollapsed
185
+ );
186
+ item.classList.toggle('mg-show-more--button--open', wasOpen);
187
+ item.textContent = initialText;
188
+ for (const [attribute, value] of [
189
+ ['aria-controls', initialControls],
190
+ ['role', initialRole],
191
+ ]) {
192
+ if (value === null) item.removeAttribute(attribute);
193
+ else item.setAttribute(attribute, value);
194
+ }
195
+ delete item.dataset.mgShowMoreInitialized;
196
+ });
65
197
  });
198
+
199
+ return () => cleanups.splice(0).forEach(cleanup => cleanup());
66
200
  }
67
201
 
68
202
  // Auto-wrap so the browser Event object is not passed as scope