@asd20/ui-next 2.21.5 → 2.21.7

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.21.7](https://github.com/academydistrict20/asd20-ui-next/compare/ui-next-v2.21.6...ui-next-v2.21.7) (2026-08-26)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * mobile scroll fixes; ios input zoom issue ([56ba4a7](https://github.com/academydistrict20/asd20-ui-next/commit/56ba4a75391744db8a4f43ff74e30b1fa0bd5f75))
9
+
10
+ ## [2.21.6](https://github.com/academydistrict20/asd20-ui-next/compare/ui-next-v2.21.5...ui-next-v2.21.6) (2026-08-25)
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * improve relevance of related pages in search results ([ecbcad7](https://github.com/academydistrict20/asd20-ui-next/commit/ecbcad7f5df39273dc6a5b1299b1c94d766d6af3))
16
+
3
17
  ## [2.21.5](https://github.com/academydistrict20/asd20-ui-next/compare/ui-next-v2.21.4...ui-next-v2.21.5) (2026-08-21)
4
18
 
5
19
 
package/README.md CHANGED
@@ -87,6 +87,20 @@ npm test
87
87
  npm run build
88
88
  ```
89
89
 
90
+ Optional harnesses are available to facilitate repeatable physical-device
91
+ checks for scrolling, touch, focus, keyboard, and viewport changes:
92
+
93
+ ```sh
94
+ npm run test:mobile:storybook
95
+ npm run test:mobile:package
96
+ ```
97
+
98
+ These commands are manual testing aids. They are not testing, CI, release, or
99
+ package-publishing requirements.
100
+
101
+ See [Physical Mobile Device Testing](docs/mobile-device-testing.md) for the
102
+ device checklist, packed-consumer workflow, options, and troubleshooting.
103
+
90
104
  Regenerate icon metadata after adding, removing, or renaming SVG icon assets:
91
105
 
92
106
  ```sh
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@asd20/ui-next",
3
- "version": "2.21.5",
3
+ "version": "2.21.7",
4
4
  "private": false,
5
5
  "description": "ASD20 UI component library for Vue 3.",
6
6
  "license": "MIT",
@@ -108,8 +108,8 @@ export default {
108
108
  border: none;
109
109
  border-radius: var(--website-shape__radius-s);
110
110
  // outline: 1px solid red;
111
- // font-size: space(0.75);
112
- font-size: space(0.625);
111
+ // Keep form text at least 16px so iOS Safari does not zoom the page on focus.
112
+ font-size: $base-font-size;
113
113
  line-height: 1;
114
114
  height: space(1.5);
115
115
  background: white;
@@ -153,6 +153,10 @@ import {
153
153
  shouldOpenInNewWindow,
154
154
  shouldShowExternalIcon,
155
155
  } from '../../../helpers/linkPolicy'
156
+ import {
157
+ acquireDocumentScrollLock,
158
+ releaseDocumentScrollLock,
159
+ } from '../../../helpers/documentScrollLock'
156
160
 
157
161
  export default {
158
162
  name: 'Asd20SiteMenu',
@@ -168,6 +172,7 @@ export default {
168
172
  focusedItemIndex: -1,
169
173
  hasFocus: false,
170
174
  keyboardTriggeredLastAction: false,
175
+ bodyScrollLock: null,
171
176
  }),
172
177
  computed: {
173
178
  activeSection() {
@@ -179,12 +184,43 @@ export default {
179
184
  return resolveCurrentHostname(this)
180
185
  },
181
186
  },
187
+ watch: {
188
+ active(newVal) {
189
+ if (newVal && !this.emptyNav) {
190
+ this.lockBodyScroll()
191
+ } else {
192
+ this.unlockBodyScroll()
193
+ }
194
+ },
195
+ emptyNav(newVal) {
196
+ if (this.active && !newVal) {
197
+ this.lockBodyScroll()
198
+ } else {
199
+ this.unlockBodyScroll()
200
+ }
201
+ },
202
+ },
182
203
  mounted() {
204
+ if (this.active && !this.emptyNav) {
205
+ this.lockBodyScroll()
206
+ }
183
207
  if (!this.emptyNav) {
184
208
  this.setFocus()
185
209
  }
186
210
  },
211
+ beforeUnmount() {
212
+ this.unlockBodyScroll()
213
+ },
187
214
  methods: {
215
+ lockBodyScroll() {
216
+ if (this.bodyScrollLock) return
217
+ this.bodyScrollLock = acquireDocumentScrollLock()
218
+ },
219
+ unlockBodyScroll() {
220
+ if (!this.bodyScrollLock) return
221
+ releaseDocumentScrollLock(this.bodyScrollLock)
222
+ this.bodyScrollLock = null
223
+ },
188
224
  onFocus(event) {
189
225
  this.hasFocus = true
190
226
  this.focusedSectionIndex = 0
@@ -380,6 +416,7 @@ export default {
380
416
  display: flex;
381
417
  flex-direction: column;
382
418
  overflow: hidden;
419
+ overscroll-behavior: none;
383
420
  &:focus {
384
421
  // outline: none;
385
422
  }
@@ -393,6 +430,8 @@ export default {
393
430
  bottom: space(7);
394
431
  background: var(--website-menu__open-background-color, white);
395
432
  overflow: auto;
433
+ overscroll-behavior: contain;
434
+ -webkit-overflow-scrolling: touch;
396
435
  // overflow-y: scroll !important;
397
436
  }
398
437
  .asd20-table-of-contents {
@@ -493,8 +532,11 @@ export default {
493
532
  }
494
533
  }
495
534
  .asd20-site-menu__items {
535
+ flex: 1 1 auto;
536
+ min-height: 0;
496
537
  overflow-y: scroll; /* has to be scroll, not auto */
497
- // -webkit-overflow-scrolling: touch;
538
+ overscroll-behavior: contain;
539
+ -webkit-overflow-scrolling: touch;
498
540
  }
499
541
  .asd20-site-menu__item {
500
542
  display: block;
@@ -40,7 +40,7 @@
40
40
  /> -->
41
41
  <asd20-action-menu
42
42
  v-show="
43
- (menuOpen && (mq === 'sm' || mq === 'md') && isLoaded && !zoomed) ||
43
+ (menuOpen && (mq === 'sm' || mq === 'md') && isLoaded) ||
44
44
  ((mq === 'lg' || mq === 'xl') && isLoaded)
45
45
  "
46
46
  :items="actionItems"
@@ -54,7 +54,6 @@
54
54
  <asd20-site-menu
55
55
  v-show="menuOpen"
56
56
  ref="siteMenu"
57
- :class="zoomed ? 'zoomed' : ''"
58
57
  :active="menuOpen"
59
58
  :sections="navigation"
60
59
  :empty-nav="emptyNav"
@@ -290,11 +289,6 @@ export default {
290
289
  &.active::before {
291
290
  opacity: 1;
292
291
  }
293
- .zoomed {
294
- & :deep(.asd20-site-menu__viewport) {
295
- bottom: 0 !important;
296
- }
297
- }
298
292
  }
299
293
 
300
294
  @media (min-width: 1024px) {
@@ -563,6 +563,10 @@ import queryAiSite from '../../../helpers/queryAiSite'
563
563
  import queryFiles from '../../../helpers/queryFiles'
564
564
  import queryPeople from '../../../helpers/queryPeople'
565
565
  import queryPages, { NO_PAGE_RESULTS_FALLBACKS } from '../../../helpers/queryPages'
566
+ import {
567
+ acquireDocumentScrollLock,
568
+ releaseDocumentScrollLock,
569
+ } from '../../../helpers/documentScrollLock'
566
570
 
567
571
  // Mixins
568
572
  import globalPropMixinFactory from '../../../mixins/globalPropMixinFactory.js'
@@ -2162,18 +2166,13 @@ export default {
2162
2166
  },
2163
2167
 
2164
2168
  lockBodyScroll() {
2165
- if (typeof window === 'undefined' || typeof document === 'undefined') {
2166
- return
2167
- }
2168
2169
  if (this.bodyScrollLock) return
2169
- this.bodyScrollLock = true
2170
+ this.bodyScrollLock = acquireDocumentScrollLock()
2170
2171
  },
2171
2172
 
2172
2173
  unlockBodyScroll() {
2173
- if (typeof window === 'undefined' || typeof document === 'undefined') {
2174
- return
2175
- }
2176
2174
  if (!this.bodyScrollLock) return
2175
+ releaseDocumentScrollLock(this.bodyScrollLock)
2177
2176
  this.bodyScrollLock = null
2178
2177
  },
2179
2178
 
@@ -2441,10 +2440,19 @@ export default {
2441
2440
  return turn.relatedPages
2442
2441
  },
2443
2442
 
2444
- buildAiRelatedPagesPreview(items = this.pageListItems) {
2445
- if (!Array.isArray(items) || !items.length) return []
2443
+ buildAiRelatedPagesPreview(items = null) {
2444
+ const previewItems = Array.isArray(items)
2445
+ ? items
2446
+ : Array.isArray(this.currentPages)
2447
+ ? this.currentPages.map(page => ({
2448
+ page,
2449
+ label: page && page.title ? page.title : '',
2450
+ link: page && page.url ? page.url : '',
2451
+ }))
2452
+ : []
2453
+ if (!previewItems.length) return []
2446
2454
 
2447
- return items
2455
+ return previewItems
2448
2456
  .map((item, index) => {
2449
2457
  const page = item && item.page ? item.page : {}
2450
2458
  if (page.isNoResultsFallback) return null
@@ -2491,12 +2499,13 @@ export default {
2491
2499
  },
2492
2500
 
2493
2501
  completeAiTurnRelatedPages(turnId, runId) {
2494
- if (!turnId || !this.isCurrentSearchRun(runId)) return false
2502
+ if (!turnId || !this.isCurrentSearchRun(runId)) return []
2503
+ const relatedPages = this.buildAiRelatedPagesPreview()
2495
2504
  this.updateAiTranscriptTurn(turnId, {
2496
- relatedPages: this.buildAiRelatedPagesPreview(),
2505
+ relatedPages,
2497
2506
  relatedPagesStatus: 'complete',
2498
2507
  })
2499
- return true
2508
+ return relatedPages
2500
2509
  },
2501
2510
 
2502
2511
  setAiTurnSearchLogId(turnId, searchLogId) {
@@ -2691,6 +2700,7 @@ export default {
2691
2700
  keywords: this.keywords,
2692
2701
  question: this.searchQuestion || this.inputText,
2693
2702
  }
2703
+ if (options.purpose) searchInput.purpose = options.purpose
2694
2704
 
2695
2705
  this.searchingPages = true
2696
2706
  try {
@@ -3116,12 +3126,16 @@ export default {
3116
3126
  this.keywordsFromAi = true
3117
3127
  this.skipKeywordWatcher = true
3118
3128
  this.keywords = keywordsToUse
3119
- await this.searchPages({ keepAnswersTab: true, runId })
3129
+ await this.searchPages({
3130
+ keepAnswersTab: true,
3131
+ purpose: 'ai-related-pages',
3132
+ runId,
3133
+ })
3120
3134
  if (!this.isCurrentSearchRun(runId)) return
3121
- this.completeAiTurnRelatedPages(assistantTurnId, runId)
3122
- relatedPagesForAnalytics = this.getAnalyticsPages()
3123
- .slice(0, 10)
3124
- .map(p => ({ title: p.title || '', url: p.url || '' }))
3135
+ relatedPagesForAnalytics = this.completeAiTurnRelatedPages(
3136
+ assistantTurnId,
3137
+ runId
3138
+ ).map(p => ({ title: p.title || '', url: p.url || '' }))
3125
3139
  this.searchFilesWithGroupOwners({ runId })
3126
3140
  }
3127
3141
 
@@ -3169,29 +3183,12 @@ export default {
3169
3183
  }
3170
3184
 
3171
3185
  // Log AI search analytics
3172
- const logPagesFromPages =
3173
- relatedPagesForAnalytics.length > 0
3174
- ? relatedPagesForAnalytics
3175
- : this.getAnalyticsPages()
3176
- .slice(0, 10)
3177
- .map(p => ({ title: p.title || '', url: p.url || '' }))
3178
- const logPagesFromSources = (
3179
- Array.isArray(this.aiSources) ? this.aiSources : []
3180
- )
3181
- .slice(0, 10)
3182
- .map(source => ({
3183
- title: source && source.title ? source.title : '',
3184
- url: source && source.url ? source.url : '',
3185
- }))
3186
- .filter(source => source.url)
3187
- const logPages =
3188
- logPagesFromPages.length > 0 ? logPagesFromPages : logPagesFromSources
3189
3186
  const logPromise = logSearchAnalytics({
3190
3187
  question: normalizedQuestion,
3191
3188
  searchType: 'ai_chat',
3192
3189
  keywords: this.keywords || '',
3193
3190
  hadResults: !!this.aiAnswer,
3194
- relatedPages: logPages,
3191
+ relatedPages: relatedPagesForAnalytics,
3195
3192
  organizationId:
3196
3193
  this.organization && this.organization.id
3197
3194
  ? this.organization.id
@@ -3603,6 +3600,7 @@ export default {
3603
3600
  display: flex;
3604
3601
  flex-direction: column;
3605
3602
  overflow: hidden;
3603
+ overscroll-behavior: none;
3606
3604
 
3607
3605
  /* allow descendants to use flex+overflow scrolling properly (Safari/iOS) */
3608
3606
  min-height: 0;
@@ -3854,6 +3852,8 @@ export default {
3854
3852
  border-radius: 0.5rem;
3855
3853
  padding: space(0.5);
3856
3854
  font: inherit;
3855
+ // Match the iOS-safe minimum used by Asd20SearchField.
3856
+ font-size: $base-font-size;
3857
3857
  color: var(--color__primary);
3858
3858
  background: white;
3859
3859
  box-sizing: border-box;
@@ -4190,11 +4190,6 @@ export default {
4190
4190
  transform: translate3d(0, -0.75rem, 0);
4191
4191
  }
4192
4192
 
4193
- :global(html.asd20-site-search--scroll-locked),
4194
- :global(body.asd20-site-search--scroll-locked) {
4195
- overscroll-behavior: none;
4196
- }
4197
-
4198
4193
  @media (min-width: 768px) {
4199
4194
  .asd20-site-search {
4200
4195
  .asd20-notificaiton {
@@ -117,6 +117,7 @@ export default {
117
117
  padding: 0 space(0.75);
118
118
  width: 100%;
119
119
  overflow-x: auto;
120
+ overflow-y: hidden;
120
121
  scrollbar-width: thin;
121
122
  }
122
123
 
@@ -0,0 +1,60 @@
1
+ const SCROLL_LOCK_CLASS = 'asd20-document-scroll-locked'
2
+
3
+ const activeLocks = new Set()
4
+ let originalDocumentStyles = null
5
+
6
+ export function acquireDocumentScrollLock() {
7
+ if (typeof document === 'undefined') return null
8
+
9
+ const docEl = document.documentElement
10
+ const body = document.body
11
+ if (!docEl || !body) return null
12
+
13
+ // A primitive token is preserved when stored in Vue reactive component data.
14
+ const lock = Symbol(SCROLL_LOCK_CLASS)
15
+
16
+ if (activeLocks.size === 0) {
17
+ originalDocumentStyles = {
18
+ bodyOverflow: body.style.overflow,
19
+ bodyOverscrollBehavior: body.style.overscrollBehavior,
20
+ docOverflow: docEl.style.overflow,
21
+ docOverscrollBehavior: docEl.style.overscrollBehavior,
22
+ bodyHadLockClass: body.classList.contains(SCROLL_LOCK_CLASS),
23
+ docHadLockClass: docEl.classList.contains(SCROLL_LOCK_CLASS),
24
+ }
25
+
26
+ body.classList.add(SCROLL_LOCK_CLASS)
27
+ docEl.classList.add(SCROLL_LOCK_CLASS)
28
+ body.style.overflow = 'hidden'
29
+ body.style.overscrollBehavior = 'none'
30
+ docEl.style.overflow = 'hidden'
31
+ docEl.style.overscrollBehavior = 'none'
32
+ }
33
+
34
+ activeLocks.add(lock)
35
+ return lock
36
+ }
37
+
38
+ export function releaseDocumentScrollLock(lock) {
39
+ if (!lock || !activeLocks.delete(lock) || activeLocks.size > 0) return
40
+ if (typeof document === 'undefined' || !originalDocumentStyles) return
41
+
42
+ const docEl = document.documentElement
43
+ const body = document.body
44
+ if (!docEl || !body) return
45
+
46
+ body.style.overflow = originalDocumentStyles.bodyOverflow
47
+ body.style.overscrollBehavior =
48
+ originalDocumentStyles.bodyOverscrollBehavior
49
+ docEl.style.overflow = originalDocumentStyles.docOverflow
50
+ docEl.style.overscrollBehavior = originalDocumentStyles.docOverscrollBehavior
51
+
52
+ if (!originalDocumentStyles.bodyHadLockClass) {
53
+ body.classList.remove(SCROLL_LOCK_CLASS)
54
+ }
55
+ if (!originalDocumentStyles.docHadLockClass) {
56
+ docEl.classList.remove(SCROLL_LOCK_CLASS)
57
+ }
58
+
59
+ originalDocumentStyles = null
60
+ }