@asd20/ui-next 2.28.1 → 2.29.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ # [2.29.0](https://github.com/academydistrict20/asd20-ui-next/compare/ui-next-v2.28.1...ui-next-v2.29.0) (2026-10-02)
4
+
5
+
6
+ ### Features
7
+
8
+ * **handbook:** add expandable H2 subtopics to index ([ac65b77](https://github.com/academydistrict20/asd20-ui-next/commit/ac65b778538ea118855cb876d712909ae038a679))
9
+
3
10
  ## [2.28.1](https://github.com/academydistrict20/asd20-ui-next/compare/ui-next-v2.28.0...ui-next-v2.28.1) (2026-10-02)
4
11
 
5
12
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@asd20/ui-next",
3
- "version": "2.28.1",
3
+ "version": "2.29.0",
4
4
  "private": false,
5
5
  "description": "ASD20 UI component library for Vue 3.",
6
6
  "license": "MIT",
@@ -78,13 +78,50 @@
78
78
  v-for="message in topicMessages"
79
79
  :key="message.id"
80
80
  >
81
- <button
82
- type="button"
83
- :aria-current="activeMessage && activeMessage.id === message.id ? 'true' : null"
84
- @click="selectTopic(message.id)"
81
+ <div class="asd20-template-handbook__topic-row">
82
+ <button
83
+ class="asd20-template-handbook__topic-button"
84
+ type="button"
85
+ :aria-current="activeMessage && activeMessage.id === message.id ? 'true' : null"
86
+ @click="selectTopic(message.id)"
87
+ >
88
+ {{ message.heading || message.title }}
89
+ </button>
90
+ <button
91
+ v-if="subtopicsFor(message.id).length"
92
+ class="asd20-template-handbook__expand-button"
93
+ type="button"
94
+ :aria-expanded="expandedTopicId === message.id"
95
+ :aria-controls="`handbook-subtopics-${message.id}`"
96
+ :aria-label="`${expandedTopicId === message.id ? 'Hide' : 'Show'} sections in ${message.heading || message.title}`"
97
+ @click="toggleTopic(message.id)"
98
+ >
99
+ <asd20-icon
100
+ name="chevron"
101
+ size="sm"
102
+ focusable="false"
103
+ />
104
+ </button>
105
+ </div>
106
+ <ol
107
+ v-if="subtopicsFor(message.id).length"
108
+ v-show="expandedTopicId === message.id"
109
+ :id="`handbook-subtopics-${message.id}`"
110
+ class="asd20-template-handbook__subtopics"
85
111
  >
86
- {{ message.heading || message.title }}
87
- </button>
112
+ <li
113
+ v-for="subtopic in subtopicsFor(message.id)"
114
+ :key="subtopic.index"
115
+ >
116
+ <button
117
+ class="asd20-template-handbook__subtopic-button"
118
+ type="button"
119
+ @click="selectSubtopic(message.id, subtopic.index)"
120
+ >
121
+ {{ subtopic.label }}
122
+ </button>
123
+ </li>
124
+ </ol>
88
125
  </li>
89
126
  </ol>
90
127
  </nav>
@@ -92,6 +129,7 @@
92
129
  <article
93
130
  v-for="message in topicMessages"
94
131
  :key="message.id"
132
+ :data-message-id="message.id"
95
133
  class="asd20-template-handbook__topic"
96
134
  :class="{ 'is-active': activeMessage && activeMessage.id === message.id }"
97
135
  >
@@ -133,6 +171,7 @@
133
171
  <script>
134
172
  import Asd20SkipTo from '../../../components/atoms/Asd20SkipTo'
135
173
  import Asd20Messaging from '../../../components/atoms/Asd20Messaging'
174
+ import Asd20Icon from '../../../components/atoms/Asd20Icon'
136
175
  import Asd20PageHeader from '../../../components/organisms/Asd20PageHeader'
137
176
  import Asd20SiteNavigation from '../../../components/organisms/Asd20SiteNavigation'
138
177
  import Asd20NotificationGroup from '../../../components/organisms/Asd20NotificationGroup'
@@ -145,6 +184,7 @@ export default {
145
184
  components: {
146
185
  Asd20SkipTo,
147
186
  Asd20Messaging,
187
+ Asd20Icon,
148
188
  Asd20PageHeader,
149
189
  Asd20SiteNavigation,
150
190
  Asd20NotificationGroup,
@@ -154,6 +194,8 @@ export default {
154
194
  mixins: [pageTemplateMixin],
155
195
  data: () => ({
156
196
  selectedMessageId: null,
197
+ expandedTopicId: null,
198
+ subtopicsByMessageId: {},
157
199
  topicsOpen: false,
158
200
  }),
159
201
  computed: {
@@ -169,13 +211,61 @@ export default {
169
211
  || null
170
212
  },
171
213
  },
214
+ watch: {
215
+ messages: {
216
+ handler() {
217
+ this.$nextTick(() => this.refreshSubtopics())
218
+ },
219
+ deep: true,
220
+ },
221
+ },
222
+ mounted() {
223
+ this.$nextTick(() => this.refreshSubtopics())
224
+ },
172
225
  methods: {
173
226
  topicSummary(message) {
174
227
  return message.shortDescription || message.summary || ''
175
228
  },
176
- selectTopic(messageId) {
229
+ topicArticle(messageId) {
230
+ return Array.from(this.$el.querySelectorAll('.asd20-template-handbook__topic'))
231
+ .find(article => article.dataset.messageId === String(messageId))
232
+ },
233
+ subtopicsFor(messageId) {
234
+ return this.subtopicsByMessageId[messageId] || []
235
+ },
236
+ refreshSubtopics() {
237
+ const nextSubtopics = {}
238
+ this.topicMessages.forEach(message => {
239
+ const article = this.topicArticle(message.id)
240
+ if (!article) return
241
+ const headings = Array.from(article.querySelectorAll('.asd20-rich-body-content h2'))
242
+ const subtopics = headings
243
+ .map((heading, index) => ({ index, label: heading.textContent.trim() }))
244
+ .filter(subtopic => subtopic.label)
245
+ if (subtopics.length) nextSubtopics[message.id] = subtopics
246
+ })
247
+ if (JSON.stringify(nextSubtopics) !== JSON.stringify(this.subtopicsByMessageId)) {
248
+ this.subtopicsByMessageId = nextSubtopics
249
+ }
250
+ },
251
+ toggleTopic(messageId) {
252
+ this.expandedTopicId = this.expandedTopicId === messageId ? null : messageId
253
+ },
254
+ async selectTopic(messageId) {
177
255
  this.selectedMessageId = messageId
256
+ this.expandedTopicId = this.subtopicsFor(messageId).length ? messageId : null
178
257
  this.topicsOpen = false
258
+ await this.$nextTick()
259
+ this.topicArticle(messageId)?.scrollIntoView({ block: 'start' })
260
+ },
261
+ async selectSubtopic(messageId, headingIndex) {
262
+ this.selectedMessageId = messageId
263
+ this.expandedTopicId = messageId
264
+ this.topicsOpen = false
265
+ await this.$nextTick()
266
+ const article = this.topicArticle(messageId)
267
+ const headings = article?.querySelectorAll('.asd20-rich-body-content h2')
268
+ headings?.[headingIndex]?.scrollIntoView({ block: 'start' })
179
269
  },
180
270
  },
181
271
  }
@@ -227,8 +317,49 @@ export default {
227
317
  padding: 0;
228
318
  }
229
319
 
230
- &__index ol.is-open {
320
+ &__index ol.is-open,
321
+ &__index ol.is-open ol.asd20-template-handbook__subtopics {
322
+ display: block;
323
+ }
324
+
325
+ &__subtopics {
326
+ padding-left: space(0.75) !important;
327
+ }
328
+
329
+ &__topic-row {
330
+ display: flex;
331
+ align-items: stretch;
332
+ }
333
+
334
+ &__topic-button {
335
+ flex: 1 1 auto;
336
+ min-width: 0;
337
+ }
338
+
339
+ &__expand-button {
340
+ display: grid;
341
+ place-items: center;
342
+ flex: 0 0 44px;
343
+ min-height: 44px;
344
+ padding: 0;
345
+ border: 0;
346
+ background: transparent;
347
+ color: inherit;
348
+ }
349
+
350
+ &__expand-button .asd20-icon {
231
351
  display: block;
352
+ color: inherit;
353
+ transform: rotate(90deg);
354
+ transition: transform 180ms ease-in-out;
355
+ }
356
+
357
+ &__expand-button[aria-expanded='true'] .asd20-icon {
358
+ transform: rotate(180deg);
359
+ }
360
+
361
+ &__subtopic-button {
362
+ font-size: 0.9em !important;
232
363
  }
233
364
 
234
365
  &__index-heading {
@@ -241,7 +372,8 @@ export default {
241
372
  cursor: pointer;
242
373
  }
243
374
 
244
- &__index li button {
375
+ &__topic-button,
376
+ &__subtopic-button {
245
377
  display: block;
246
378
  width: 100%;
247
379
  min-height: 44px;
@@ -276,6 +408,11 @@ export default {
276
408
 
277
409
  &__topic {
278
410
  min-width: 0;
411
+ scroll-margin-top: space(2);
412
+ }
413
+
414
+ &__topic .asd20-rich-body-content h2 {
415
+ scroll-margin-top: space(2);
279
416
  }
280
417
 
281
418
  &__topic:not(.is-active) {
@@ -287,8 +424,8 @@ export default {
287
424
  .asd20-template-handbook {
288
425
  &__layout {
289
426
  grid-template-columns: minmax(12rem, 16rem) minmax(0, 1fr);
290
- gap: space(3);
291
- padding: space(3);
427
+ gap: space(2);
428
+ padding: space(2);
292
429
  }
293
430
 
294
431
  &__index {
@@ -314,6 +451,17 @@ export default {
314
451
  .asd20-template-handbook {
315
452
  margin-left: space(3);
316
453
  margin-top: 0;
454
+ &__layout {
455
+ grid-template-columns: minmax(12rem, 22rem) minmax(0, 1fr);
456
+ gap: space(3);
457
+ padding: space(3);
458
+ }
459
+ }
460
+ }
461
+
462
+ @media (prefers-reduced-motion: reduce) {
463
+ .asd20-template-handbook__expand-button .asd20-icon {
464
+ transition: none;
317
465
  }
318
466
  }
319
467