@asd20/ui-next 2.28.0 → 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,19 @@
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
+
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)
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * allow an intro message on the handbook page; fix unnecessary focus outlines ([e8c8e09](https://github.com/academydistrict20/asd20-ui-next/commit/e8c8e0965f1f4cef4eb2b8ffa9aacb9b82c0ccc0))
16
+
3
17
  # [2.28.0](https://github.com/academydistrict20/asd20-ui-next/compare/ui-next-v2.27.2...ui-next-v2.28.0) (2026-10-02)
4
18
 
5
19
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@asd20/ui-next",
3
- "version": "2.28.0",
3
+ "version": "2.29.0",
4
4
  "private": false,
5
5
  "description": "ASD20 UI component library for Vue 3.",
6
6
  "license": "MIT",
@@ -23,7 +23,7 @@
23
23
 
24
24
  <asd20-page-header
25
25
  :heading="title"
26
- :lead="metaDescription"
26
+ :lead="handbookLead"
27
27
  :organization="organization"
28
28
  :organization-options="organizationOptions"
29
29
  :breadcrumb-links="breadcrumbLinks"
@@ -75,38 +75,84 @@
75
75
  :class="{ 'is-open': topicsOpen }"
76
76
  >
77
77
  <li
78
- v-for="message in messages"
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>
91
128
 
92
129
  <article
93
- v-for="message in messages"
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
  >
98
- <h2
99
- tabindex="-1"
100
- >
136
+ <h2>
101
137
  {{ message.heading || message.title }}
102
138
  </h2>
139
+ <div
140
+ v-if="topicSummary(message)"
141
+ class="asd20-template-handbook__summary"
142
+ v-html="topicSummary(message)"
143
+ />
144
+ <hr
145
+ v-if="topicSummary(message)"
146
+ class="asd20-template-handbook__summary-divider"
147
+ />
103
148
  <asd20-messaging
104
149
  v-bind="message"
105
150
  heading=""
151
+ short-description=""
106
152
  detail-link=""
107
153
  />
108
154
  </article>
109
- <p v-if="!messages.length">
155
+ <p v-if="!topicMessages.length">
110
156
  No handbook topics are available yet.
111
157
  </p>
112
158
  </main>
@@ -123,9 +169,9 @@
123
169
  </template>
124
170
 
125
171
  <script>
126
- import { nextTick } from 'vue'
127
172
  import Asd20SkipTo from '../../../components/atoms/Asd20SkipTo'
128
173
  import Asd20Messaging from '../../../components/atoms/Asd20Messaging'
174
+ import Asd20Icon from '../../../components/atoms/Asd20Icon'
129
175
  import Asd20PageHeader from '../../../components/organisms/Asd20PageHeader'
130
176
  import Asd20SiteNavigation from '../../../components/organisms/Asd20SiteNavigation'
131
177
  import Asd20NotificationGroup from '../../../components/organisms/Asd20NotificationGroup'
@@ -138,6 +184,7 @@ export default {
138
184
  components: {
139
185
  Asd20SkipTo,
140
186
  Asd20Messaging,
187
+ Asd20Icon,
141
188
  Asd20PageHeader,
142
189
  Asd20SiteNavigation,
143
190
  Asd20NotificationGroup,
@@ -147,21 +194,78 @@ export default {
147
194
  mixins: [pageTemplateMixin],
148
195
  data: () => ({
149
196
  selectedMessageId: null,
197
+ expandedTopicId: null,
198
+ subtopicsByMessageId: {},
150
199
  topicsOpen: false,
151
200
  }),
152
201
  computed: {
202
+ handbookLead() {
203
+ return this.firstMessage?.shortDescription || this.firstMessage?.summary || this.metaDescription
204
+ },
205
+ topicMessages() {
206
+ return this.messages.slice(1)
207
+ },
153
208
  activeMessage() {
154
- return this.messages.find(message => message.id === this.selectedMessageId)
155
- || this.messages[0]
209
+ return this.topicMessages.find(message => message.id === this.selectedMessageId)
210
+ || this.topicMessages[0]
156
211
  || null
157
212
  },
158
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
+ },
159
225
  methods: {
226
+ topicSummary(message) {
227
+ return message.shortDescription || message.summary || ''
228
+ },
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
+ },
160
254
  async selectTopic(messageId) {
161
255
  this.selectedMessageId = messageId
256
+ this.expandedTopicId = this.subtopicsFor(messageId).length ? messageId : null
162
257
  this.topicsOpen = false
163
- await nextTick()
164
- this.$el.querySelector('.asd20-template-handbook__topic.is-active h2')?.focus()
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' })
165
269
  },
166
270
  },
167
271
  }
@@ -189,6 +293,23 @@ export default {
189
293
  padding: space(2) space(1);
190
294
  }
191
295
 
296
+ &__layout:focus:not(.asd20-tab):not([role='tab']):not(.asd20-focus-trap) {
297
+ outline: none;
298
+ box-shadow: none !important;
299
+ }
300
+
301
+ &__summary {
302
+ @include asd20-font(0.75, var(--website-typography__font-family-body), 1.5);
303
+ margin-bottom: space(0.5);
304
+ }
305
+
306
+ &__summary-divider {
307
+ margin: 0 0 space(1);
308
+ border: 0;
309
+ border-top: 1px solid currentColor;
310
+ opacity: 0.25;
311
+ }
312
+
192
313
  &__index ol {
193
314
  display: none;
194
315
  list-style: none;
@@ -196,10 +317,51 @@ export default {
196
317
  padding: 0;
197
318
  }
198
319
 
199
- &__index ol.is-open {
320
+ &__index ol.is-open,
321
+ &__index ol.is-open ol.asd20-template-handbook__subtopics {
200
322
  display: block;
201
323
  }
202
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 {
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;
363
+ }
364
+
203
365
  &__index-heading {
204
366
  display: none;
205
367
  }
@@ -210,7 +372,8 @@ export default {
210
372
  cursor: pointer;
211
373
  }
212
374
 
213
- &__index li button {
375
+ &__topic-button,
376
+ &__subtopic-button {
214
377
  display: block;
215
378
  width: 100%;
216
379
  min-height: 44px;
@@ -245,6 +408,11 @@ export default {
245
408
 
246
409
  &__topic {
247
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);
248
416
  }
249
417
 
250
418
  &__topic:not(.is-active) {
@@ -256,8 +424,8 @@ export default {
256
424
  .asd20-template-handbook {
257
425
  &__layout {
258
426
  grid-template-columns: minmax(12rem, 16rem) minmax(0, 1fr);
259
- gap: space(3);
260
- padding: space(3);
427
+ gap: space(2);
428
+ padding: space(2);
261
429
  }
262
430
 
263
431
  &__index {
@@ -283,6 +451,17 @@ export default {
283
451
  .asd20-template-handbook {
284
452
  margin-left: space(3);
285
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;
286
465
  }
287
466
  }
288
467