@asd20/ui-next 2.3.0 → 2.3.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.3.2](https://github.com/academydistrict20/asd20-ui-next/compare/ui-next-v2.3.1...ui-next-v2.3.2) (2026-04-14)
4
+
5
+ ## [2.3.1](https://github.com/academydistrict20/asd20-ui-next/compare/ui-next-v2.3.0...ui-next-v2.3.1) (2026-04-14)
6
+
7
+
8
+ ### Bug Fixes
9
+
10
+ * fix gallery zoom; harden accordion clicks ([da17e1e](https://github.com/academydistrict20/asd20-ui-next/commit/da17e1e82cdaf73a0468e630810f2c79dfaf2d26))
11
+
3
12
  # [2.3.0](https://github.com/academydistrict20/asd20-ui-next/compare/ui-next-v2.2.2...ui-next-v2.3.0) (2026-04-13)
4
13
 
5
14
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@asd20/ui-next",
3
- "version": "2.3.0",
3
+ "version": "2.3.2",
4
4
  "private": false,
5
5
  "description": "ASD20 UI component library for Vue 3.",
6
6
  "license": "MIT",
@@ -23,7 +23,7 @@
23
23
  <slot name="indicator">
24
24
  <div class="asd20-swipe__indicator__list">
25
25
  <span
26
- v-for="n in $children.length"
26
+ v-for="n in slideCount"
27
27
  :key="n - 1"
28
28
  class="asd20-swipe__indicator__item"
29
29
  :style="{
@@ -43,7 +43,7 @@
43
43
  </slot>
44
44
  </div>
45
45
  <div
46
- v-if="showArrows && childLen > 1"
46
+ v-if="showArrows && slideCount > 1"
47
47
  class="asd20-swipe__arrows"
48
48
  >
49
49
  <button
@@ -116,6 +116,7 @@ export default {
116
116
  lastDiff: 0,
117
117
  cur: 0,
118
118
  index: 0,
119
+ slideCount: 0,
119
120
  childLen: 0,
120
121
  leftBorder: 0,
121
122
  rightBorder: 0,
@@ -140,6 +141,7 @@ export default {
140
141
  this.init()
141
142
  },
142
143
  beforeUnmount() {
144
+ this.unbindEvents()
143
145
  this.clearAutoplayTimer()
144
146
  },
145
147
 
@@ -151,13 +153,15 @@ export default {
151
153
  this.wrapper = this.$refs['asd20-swipe__wrapper']
152
154
  this.container = this.$refs['asd20-swipe__container']
153
155
  this.wrapWidth = this.wrapper.clientWidth
154
- this.childLen = this.$children.length
156
+ this.slideCount = this.getSlideElements().length
157
+ this.childLen = this.slideCount
155
158
 
156
159
  if (this.isLoopNeeded && this.childLen > 1) {
157
- const firstChild = this.$children[0].$el.cloneNode(true)
158
- const lastChild = this.$children[this.childLen - 1].$el.cloneNode(true)
160
+ const slideElements = this.getSlideElements()
161
+ const firstChild = slideElements[0].cloneNode(true)
162
+ const lastChild = slideElements[this.childLen - 1].cloneNode(true)
159
163
  this.container.appendChild(firstChild)
160
- this.container.insertBefore(lastChild, this.$children[0].$el)
164
+ this.container.insertBefore(lastChild, slideElements[0])
161
165
  this.childLen += 2
162
166
  this.cur = 1
163
167
  this.diff = -this.wrapWidth
@@ -172,6 +176,9 @@ export default {
172
176
  this.autoSwipe()
173
177
  }
174
178
  },
179
+ getSlideElements() {
180
+ return Array.from(this.container?.children || [])
181
+ },
175
182
 
176
183
  bindEvents() {
177
184
  this.wrapper.addEventListener('touchstart', this.handleTouchStart)
@@ -182,25 +189,44 @@ export default {
182
189
  this.wrapper.addEventListener('mousemove', this.handleMouseMove)
183
190
  this.wrapper.addEventListener('mouseup', this.handleMouseUp)
184
191
  }
185
- this.container.addEventListener('transitionend', () => {
186
- this.dur = 0
187
- if (this.isLoopNeeded) {
188
- if (this.cur === 0) {
189
- this.cur = this.childLen - 2
190
- this.diff = -this.wrapWidth * this.cur
191
- } else if (this.cur === this.childLen - 1) {
192
- this.cur = 1
193
- this.diff = -this.wrapWidth
194
- }
195
- }
196
- this.index = this.isLoopNeeded ? this.cur - 1 : this.cur
197
- this.$emit('change', this.index)
198
- this.lastDiff = this.diff
199
- this.lock = false
200
- if (this.isAutoPlayNeeded) {
201
- this.autoSwipe()
192
+ this.container.addEventListener('transitionend', this.handleTransitionEnd)
193
+ },
194
+
195
+ unbindEvents() {
196
+ if (!this.wrapper || !this.container) {
197
+ return
198
+ }
199
+
200
+ this.wrapper.removeEventListener('touchstart', this.handleTouchStart)
201
+ this.wrapper.removeEventListener('touchmove', this.handleTouchMove)
202
+ this.wrapper.removeEventListener('touchend', this.handleTouchEnd)
203
+ this.wrapper.removeEventListener('mousedown', this.handleMouseDown)
204
+ this.wrapper.removeEventListener('mousemove', this.handleMouseMove)
205
+ this.wrapper.removeEventListener('mouseup', this.handleMouseUp)
206
+ this.container.removeEventListener(
207
+ 'transitionend',
208
+ this.handleTransitionEnd
209
+ )
210
+ },
211
+
212
+ handleTransitionEnd() {
213
+ this.dur = 0
214
+ if (this.isLoopNeeded) {
215
+ if (this.cur === 0) {
216
+ this.cur = this.childLen - 2
217
+ this.diff = -this.wrapWidth * this.cur
218
+ } else if (this.cur === this.childLen - 1) {
219
+ this.cur = 1
220
+ this.diff = -this.wrapWidth
202
221
  }
203
- })
222
+ }
223
+ this.index = this.isLoopNeeded ? this.cur - 1 : this.cur
224
+ this.$emit('change', this.index)
225
+ this.lastDiff = this.diff
226
+ this.lock = false
227
+ if (this.isAutoPlayNeeded) {
228
+ this.autoSwipe()
229
+ }
204
230
  },
205
231
 
206
232
  handleTouchStart(event) {
@@ -264,7 +290,6 @@ export default {
264
290
  },
265
291
 
266
292
  handleMouseDown(event) {
267
- console.log(event)
268
293
  if (this.lock) {
269
294
  return
270
295
  }
@@ -351,6 +376,8 @@ export default {
351
376
  this.dur = this.duration
352
377
  this.diff = -this.wrapWidth * this.cur
353
378
  }
379
+ this.index = this.isLoopNeeded ? this.cur - 1 : this.cur
380
+ this.lastDiff = this.diff
354
381
  },
355
382
  download() {
356
383
  window.open(this.imageThumbnails[this.index].url, '_blank')
@@ -157,6 +157,9 @@ export default {
157
157
  this.handleResize()
158
158
  window.addEventListener('resize', this.handleResize)
159
159
  },
160
+ beforeUnmount() {
161
+ window.removeEventListener('resize', this.handleResize)
162
+ },
160
163
 
161
164
  methods: {
162
165
  handleResize() {
@@ -37,6 +37,7 @@
37
37
  >
38
38
  <span
39
39
  v-if="totalDismissedNotifications > 0"
40
+ class="bell__count"
40
41
  aria-hidden="true"
41
42
  >{{
42
43
  totalDismissedNotifications
@@ -315,7 +316,7 @@ export default {
315
316
  fill: var(--website-menu__icon-fill-color) !important;
316
317
  }
317
318
 
318
- span {
319
+ .bell__count {
319
320
  position: absolute;
320
321
  top: -0.25em;
321
322
  right: -0.25em;
@@ -157,6 +157,9 @@ export default {
157
157
  this.handleResize()
158
158
  window.addEventListener('resize', this.handleResize)
159
159
  },
160
+ beforeUnmount() {
161
+ window.removeEventListener('resize', this.handleResize)
162
+ },
160
163
 
161
164
  methods: {
162
165
  handleResize() {
@@ -217,6 +217,9 @@ export default {
217
217
  this.zoomed = window.innerHeight <= 500
218
218
  window.addEventListener('resize', this.handleResize)
219
219
  },
220
+ beforeUnmount() {
221
+ window.removeEventListener('resize', this.handleResize)
222
+ },
220
223
  methods: {
221
224
  handleResize() {
222
225
  this.zoomed = window.innerHeight <= 500
@@ -164,6 +164,7 @@ import Asd20NotificationGroup from '../../../components/organisms/Asd20Notificat
164
164
 
165
165
  // Mixins
166
166
  import pageTemplateMixin from '../../../mixins/pageTemplateMixin'
167
+ import { bindInjectedAccordionInteractions } from '../../../helpers/injectedContentInteractions'
167
168
 
168
169
  export default {
169
170
  name: 'Asd20DetailTemplate',
@@ -181,54 +182,12 @@ export default {
181
182
  },
182
183
  mixins: [pageTemplateMixin],
183
184
  mounted() {
184
- // Allow accordions to be injected into the DOM via the SuperEditor in the CommCenter
185
- this.$nextTick(() => {
186
- const accordionButtons = document.querySelectorAll('.accordion-button')
187
-
188
- accordionButtons.forEach(button => {
189
- button.addEventListener('click', () => {
190
- const contentId = button.getAttribute('aria-controls')
191
- const content = document.getElementById(contentId)
192
- const expanded = button.getAttribute('aria-expanded') === 'true'
193
-
194
- button.setAttribute('aria-expanded', !expanded)
195
- button.querySelector('.toggle-icon').textContent = expanded
196
- ? '+'
197
- : '-'
198
-
199
- if (!expanded) {
200
- content.style.display = 'block'
201
-
202
- // Reset maxHeight before calculating scrollHeight (force reflow)
203
- content.style.maxHeight = '0'
204
- content.style.padding = '0 var(--space-1, 1em)'
205
-
206
- requestAnimationFrame(() => {
207
- content.style.transition =
208
- 'max-height 0.4s ease, padding 0.4s ease'
209
- content.style.maxHeight = content.scrollHeight + 'px'
210
- content.style.padding = 'var(--space-1, 1em)'
211
- })
212
- } else {
213
- content.style.maxHeight = content.scrollHeight + 'px' // Set to current height for smooth transition
214
-
215
- requestAnimationFrame(() => {
216
- content.style.transition =
217
- 'max-height 0.4s ease, padding 0.4s ease'
218
- content.style.maxHeight = '0'
219
- content.style.padding = '0 var(--space-1, 1em)'
220
- })
221
-
222
- // Optionally hide the content after animation
223
- setTimeout(() => {
224
- if (content.style.maxHeight === '0px') {
225
- content.style.display = 'none'
226
- }
227
- }, 400) // Match transition duration
228
- }
229
- })
230
- })
231
- })
185
+ this.removeInjectedAccordionInteractions = bindInjectedAccordionInteractions(
186
+ this.$el
187
+ )
188
+ },
189
+ beforeUnmount() {
190
+ this.removeInjectedAccordionInteractions?.()
232
191
  },
233
192
  }
234
193
  </script>
@@ -156,6 +156,7 @@ import Asd20NotificationGroup from '../../../components/organisms/Asd20Notificat
156
156
 
157
157
  // Mixins
158
158
  import pageTemplateMixin from '../../../mixins/pageTemplateMixin'
159
+ import { bindInjectedAccordionInteractions } from '../../../helpers/injectedContentInteractions'
159
160
 
160
161
  export default {
161
162
  name: 'Asd20DetailImageFullTemplate',
@@ -173,54 +174,12 @@ export default {
173
174
  },
174
175
  mixins: [pageTemplateMixin],
175
176
  mounted() {
176
- // Allow accordions to be injected into the DOM via the SuperEditor in the CommCenter
177
- this.$nextTick(() => {
178
- const accordionButtons = document.querySelectorAll('.accordion-button')
179
-
180
- accordionButtons.forEach(button => {
181
- button.addEventListener('click', () => {
182
- const contentId = button.getAttribute('aria-controls')
183
- const content = document.getElementById(contentId)
184
- const expanded = button.getAttribute('aria-expanded') === 'true'
185
-
186
- button.setAttribute('aria-expanded', !expanded)
187
- button.querySelector('.toggle-icon').textContent = expanded
188
- ? '+'
189
- : '-'
190
-
191
- if (!expanded) {
192
- content.style.display = 'block'
193
-
194
- // Reset maxHeight before calculating scrollHeight (force reflow)
195
- content.style.maxHeight = '0'
196
- content.style.padding = '0 var(--space-1, 1em)'
197
-
198
- requestAnimationFrame(() => {
199
- content.style.transition =
200
- 'max-height 0.4s ease, padding 0.4s ease'
201
- content.style.maxHeight = content.scrollHeight + 'px'
202
- content.style.padding = 'var(--space-1, 1em)'
203
- })
204
- } else {
205
- content.style.maxHeight = content.scrollHeight + 'px' // Set to current height for smooth transition
206
-
207
- requestAnimationFrame(() => {
208
- content.style.transition =
209
- 'max-height 0.4s ease, padding 0.4s ease'
210
- content.style.maxHeight = '0'
211
- content.style.padding = '0 var(--space-1, 1em)'
212
- })
213
-
214
- // Optionally hide the content after animation
215
- setTimeout(() => {
216
- if (content.style.maxHeight === '0px') {
217
- content.style.display = 'none'
218
- }
219
- }, 400) // Match transition duration
220
- }
221
- })
222
- })
223
- })
177
+ this.removeInjectedAccordionInteractions = bindInjectedAccordionInteractions(
178
+ this.$el
179
+ )
180
+ },
181
+ beforeUnmount() {
182
+ this.removeInjectedAccordionInteractions?.()
224
183
  },
225
184
  }
226
185
  </script>
@@ -149,6 +149,7 @@ import Asd20NotificationGroup from '../../../components/organisms/Asd20Notificat
149
149
 
150
150
  // Mixins
151
151
  import pageTemplateMixin from '../../../mixins/pageTemplateMixin'
152
+ import { bindInjectedAccordionInteractions } from '../../../helpers/injectedContentInteractions'
152
153
 
153
154
  export default {
154
155
  name: 'Asd20DetailImageTemplate',
@@ -166,54 +167,12 @@ export default {
166
167
  },
167
168
  mixins: [pageTemplateMixin],
168
169
  mounted() {
169
- // Allow accordions to be injected into the DOM via the SuperEditor in the CommCenter
170
- this.$nextTick(() => {
171
- const accordionButtons = document.querySelectorAll('.accordion-button')
172
-
173
- accordionButtons.forEach(button => {
174
- button.addEventListener('click', () => {
175
- const contentId = button.getAttribute('aria-controls')
176
- const content = document.getElementById(contentId)
177
- const expanded = button.getAttribute('aria-expanded') === 'true'
178
-
179
- button.setAttribute('aria-expanded', !expanded)
180
- button.querySelector('.toggle-icon').textContent = expanded
181
- ? '+'
182
- : '-'
183
-
184
- if (!expanded) {
185
- content.style.display = 'block'
186
-
187
- // Reset maxHeight before calculating scrollHeight (force reflow)
188
- content.style.maxHeight = '0'
189
- content.style.padding = '0 var(--space-1, 1em)'
190
-
191
- requestAnimationFrame(() => {
192
- content.style.transition =
193
- 'max-height 0.4s ease, padding 0.4s ease'
194
- content.style.maxHeight = content.scrollHeight + 'px'
195
- content.style.padding = 'var(--space-1, 1em)'
196
- })
197
- } else {
198
- content.style.maxHeight = content.scrollHeight + 'px' // Set to current height for smooth transition
199
-
200
- requestAnimationFrame(() => {
201
- content.style.transition =
202
- 'max-height 0.4s ease, padding 0.4s ease'
203
- content.style.maxHeight = '0'
204
- content.style.padding = '0 var(--space-1, 1em)'
205
- })
206
-
207
- // Optionally hide the content after animation
208
- setTimeout(() => {
209
- if (content.style.maxHeight === '0px') {
210
- content.style.display = 'none'
211
- }
212
- }, 400) // Match transition duration
213
- }
214
- })
215
- })
216
- })
170
+ this.removeInjectedAccordionInteractions = bindInjectedAccordionInteractions(
171
+ this.$el
172
+ )
173
+ },
174
+ beforeUnmount() {
175
+ this.removeInjectedAccordionInteractions?.()
217
176
  },
218
177
  }
219
178
  </script>
@@ -149,6 +149,7 @@ import Asd20NotificationGroup from '../../../components/organisms/Asd20Notificat
149
149
 
150
150
  // Mixins
151
151
  import pageTemplateMixin from '../../../mixins/pageTemplateMixin'
152
+ import { bindInjectedAccordionInteractions } from '../../../helpers/injectedContentInteractions'
152
153
 
153
154
  export default {
154
155
  name: 'Asd20DetailTemplate',
@@ -169,54 +170,12 @@ export default {
169
170
  languageCode: { type: String, default: 'en' },
170
171
  },
171
172
  mounted() {
172
- // Allow accordions to be injected into the DOM via the SuperEditor in the CommCenter
173
- this.$nextTick(() => {
174
- const accordionButtons = document.querySelectorAll('.accordion-button')
175
-
176
- accordionButtons.forEach(button => {
177
- button.addEventListener('click', () => {
178
- const contentId = button.getAttribute('aria-controls')
179
- const content = document.getElementById(contentId)
180
- const expanded = button.getAttribute('aria-expanded') === 'true'
181
-
182
- button.setAttribute('aria-expanded', !expanded)
183
- button.querySelector('.toggle-icon').textContent = expanded
184
- ? '+'
185
- : '-'
186
-
187
- if (!expanded) {
188
- content.style.display = 'block'
189
-
190
- // Reset maxHeight before calculating scrollHeight (force reflow)
191
- content.style.maxHeight = '0'
192
- content.style.padding = '0 var(--space-1, 1em)'
193
-
194
- requestAnimationFrame(() => {
195
- content.style.transition =
196
- 'max-height 0.4s ease, padding 0.4s ease'
197
- content.style.maxHeight = content.scrollHeight + 'px'
198
- content.style.padding = 'var(--space-1, 1em)'
199
- })
200
- } else {
201
- content.style.maxHeight = content.scrollHeight + 'px' // Set to current height for smooth transition
202
-
203
- requestAnimationFrame(() => {
204
- content.style.transition =
205
- 'max-height 0.4s ease, padding 0.4s ease'
206
- content.style.maxHeight = '0'
207
- content.style.padding = '0 var(--space-1, 1em)'
208
- })
209
-
210
- // Optionally hide the content after animation
211
- setTimeout(() => {
212
- if (content.style.maxHeight === '0px') {
213
- content.style.display = 'none'
214
- }
215
- }, 400) // Match transition duration
216
- }
217
- })
218
- })
219
- })
173
+ this.removeInjectedAccordionInteractions = bindInjectedAccordionInteractions(
174
+ this.$el
175
+ )
176
+ },
177
+ beforeUnmount() {
178
+ this.removeInjectedAccordionInteractions?.()
220
179
  },
221
180
  }
222
181
  </script>
@@ -303,6 +303,9 @@ export default {
303
303
  this.$emit('events-in-view')
304
304
  }
305
305
  },
306
+ beforeUnmount() {
307
+ window.removeEventListener('resize', this.handleResize)
308
+ },
306
309
  methods: {
307
310
  handleResize() {
308
311
  this.showLogo = window.innerWidth >= 1350
@@ -167,6 +167,7 @@ import Asd20NotificationGroup from '../../../components/organisms/Asd20Notificat
167
167
 
168
168
  // Mixins
169
169
  import pageTemplateMixin from '../../../mixins/pageTemplateMixin'
170
+ import { bindAccessibilityModalTrigger } from '../../../helpers/injectedContentInteractions'
170
171
 
171
172
  export default {
172
173
  name: 'Asd20WayfindingAccessibilityTemplate',
@@ -209,17 +210,18 @@ export default {
209
210
  },
210
211
  },
211
212
  mounted() {
212
- this.$nextTick(() => {
213
- const btn = document.querySelector('[data-trigger-accessibility-modal]')
214
- if (btn) {
215
- btn.addEventListener('click', () => {
216
- this.modalVisible = true
217
- this.$nextTick(() => {
218
- this.modalOpen = true
219
- })
213
+ this.removeAccessibilityModalTrigger = bindAccessibilityModalTrigger(
214
+ this.$el,
215
+ () => {
216
+ this.modalVisible = true
217
+ this.$nextTick(() => {
218
+ this.modalOpen = true
220
219
  })
221
220
  }
222
- })
221
+ )
222
+ },
223
+ beforeUnmount() {
224
+ this.removeAccessibilityModalTrigger?.()
223
225
  },
224
226
  }
225
227
  </script>
@@ -0,0 +1,102 @@
1
+ function findClosestInRoot(event, selector, root) {
2
+ if (!(event.target instanceof Element)) {
3
+ return null
4
+ }
5
+
6
+ const match = event.target.closest(selector)
7
+
8
+ if (!match || !root.contains(match)) {
9
+ return null
10
+ }
11
+
12
+ return match
13
+ }
14
+
15
+ export function bindInjectedAccordionInteractions(root) {
16
+ if (!root?.addEventListener) {
17
+ return () => {}
18
+ }
19
+
20
+ const onClick = event => {
21
+ const button = findClosestInRoot(event, '.accordion-button', root)
22
+
23
+ if (!button) {
24
+ return
25
+ }
26
+
27
+ const contentId = button.getAttribute('aria-controls')
28
+ const content = contentId ? document.getElementById(contentId) : null
29
+
30
+ if (!content) {
31
+ return
32
+ }
33
+
34
+ const expanded = button.getAttribute('aria-expanded') === 'true'
35
+ const toggleIcon = button.querySelector('.toggle-icon')
36
+
37
+ button.setAttribute('aria-expanded', String(!expanded))
38
+
39
+ if (toggleIcon) {
40
+ toggleIcon.textContent = expanded ? '+' : '-'
41
+ }
42
+
43
+ if (!expanded) {
44
+ content.style.display = 'block'
45
+ content.style.maxHeight = '0'
46
+ content.style.padding = '0 var(--space-1, 1em)'
47
+
48
+ requestAnimationFrame(() => {
49
+ content.style.transition = 'max-height 0.4s ease, padding 0.4s ease'
50
+ content.style.maxHeight = `${content.scrollHeight}px`
51
+ content.style.padding = 'var(--space-1, 1em)'
52
+ })
53
+ return
54
+ }
55
+
56
+ content.style.maxHeight = `${content.scrollHeight}px`
57
+
58
+ requestAnimationFrame(() => {
59
+ content.style.transition = 'max-height 0.4s ease, padding 0.4s ease'
60
+ content.style.maxHeight = '0'
61
+ content.style.padding = '0 var(--space-1, 1em)'
62
+ })
63
+
64
+ setTimeout(() => {
65
+ if (content.style.maxHeight === '0px' || content.style.maxHeight === '0') {
66
+ content.style.display = 'none'
67
+ }
68
+ }, 400)
69
+ }
70
+
71
+ root.addEventListener('click', onClick)
72
+
73
+ return () => {
74
+ root.removeEventListener('click', onClick)
75
+ }
76
+ }
77
+
78
+ export function bindAccessibilityModalTrigger(root, onOpen) {
79
+ if (!root?.addEventListener) {
80
+ return () => {}
81
+ }
82
+
83
+ const onClick = event => {
84
+ const trigger = findClosestInRoot(
85
+ event,
86
+ '[data-trigger-accessibility-modal]',
87
+ root
88
+ )
89
+
90
+ if (!trigger) {
91
+ return
92
+ }
93
+
94
+ onOpen()
95
+ }
96
+
97
+ root.addEventListener('click', onClick)
98
+
99
+ return () => {
100
+ root.removeEventListener('click', onClick)
101
+ }
102
+ }
@@ -450,6 +450,9 @@ export default {
450
450
  this.desktop = window.innerWidth >= 1024
451
451
  window.addEventListener('resize', this.handleResize)
452
452
  },
453
+ beforeUnmount() {
454
+ window.removeEventListener('resize', this.handleResize)
455
+ },
453
456
  methods: {
454
457
  handleResize() {
455
458
  this.desktop = window.innerWidth >= 1024