@asd20/ui-next 2.22.0 → 2.22.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 +9 -0
- package/package.json +1 -1
- package/src/components/molecules/Asd20Modal/index.vue +30 -1
- package/src/components/molecules/Asd20Tab/index.vue +10 -1
- package/src/components/organisms/Asd20Navbar/index.vue +10 -4
- package/src/components/organisms/Asd20TabBar/index.vue +40 -0
- package/src/components/organisms/Asd20VideoHeaderContent/index.vue +2 -1
- package/src/components/templates/Asd20DistrictVideoTemplate/index.vue +1 -1
- package/src/components/templates/Asd20LoginsTemplate/index.vue +100 -83
- package/src/components/utils/FocusTrap.vue +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [2.22.2](https://github.com/academydistrict20/asd20-ui-next/compare/ui-next-v2.22.1...ui-next-v2.22.2) (2026-08-31)
|
|
4
|
+
|
|
5
|
+
## [2.22.1](https://github.com/academydistrict20/asd20-ui-next/compare/ui-next-v2.22.0...ui-next-v2.22.1) (2026-08-28)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
* accessibility improvements; native space/enter activation in nav; fix skip-link; logins tab relationships ([e198494](https://github.com/academydistrict20/asd20-ui-next/commit/e198494e9aa59b83857cf8ca8c6d933afa32e284))
|
|
11
|
+
|
|
3
12
|
# [2.22.0](https://github.com/academydistrict20/asd20-ui-next/compare/ui-next-v2.21.11...ui-next-v2.22.0) (2026-08-28)
|
|
4
13
|
|
|
5
14
|
|
package/package.json
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
>
|
|
15
15
|
<focus-trap
|
|
16
16
|
:open="resolvedOpen"
|
|
17
|
+
:return-focus-on-deactivate="false"
|
|
17
18
|
>
|
|
18
19
|
<div
|
|
19
20
|
class="asd20-modal"
|
|
@@ -84,6 +85,7 @@ export default {
|
|
|
84
85
|
listeningForEscape: false,
|
|
85
86
|
transitioning: false,
|
|
86
87
|
titleId: createComponentInstanceId('asd20-modal-title'),
|
|
88
|
+
previouslyFocusedElement: null,
|
|
87
89
|
}
|
|
88
90
|
},
|
|
89
91
|
computed: {
|
|
@@ -103,17 +105,23 @@ export default {
|
|
|
103
105
|
},
|
|
104
106
|
},
|
|
105
107
|
watch: {
|
|
106
|
-
resolvedOpen: function(newVal) {
|
|
108
|
+
resolvedOpen: function(newVal, oldVal) {
|
|
107
109
|
if (newVal) {
|
|
110
|
+
if (!oldVal) this.rememberFocus()
|
|
108
111
|
this.startListeningForEscape()
|
|
109
112
|
this.setFocus()
|
|
110
113
|
return
|
|
111
114
|
}
|
|
112
115
|
|
|
113
116
|
this.stopListeningForEscape()
|
|
117
|
+
if (oldVal) this.restoreFocus()
|
|
114
118
|
},
|
|
115
119
|
},
|
|
116
120
|
|
|
121
|
+
beforeMount() {
|
|
122
|
+
if (this.resolvedOpen) this.rememberFocus()
|
|
123
|
+
},
|
|
124
|
+
|
|
117
125
|
mounted() {
|
|
118
126
|
if (this.resolvedOpen === true) {
|
|
119
127
|
this.startListeningForEscape()
|
|
@@ -122,6 +130,7 @@ export default {
|
|
|
122
130
|
},
|
|
123
131
|
beforeUnmount() {
|
|
124
132
|
this.stopListeningForEscape()
|
|
133
|
+
if (this.resolvedOpen) this.restoreFocus()
|
|
125
134
|
},
|
|
126
135
|
methods: {
|
|
127
136
|
dismiss(value = false) {
|
|
@@ -166,6 +175,26 @@ export default {
|
|
|
166
175
|
focusTarget.focus()
|
|
167
176
|
}
|
|
168
177
|
},
|
|
178
|
+
rememberFocus() {
|
|
179
|
+
const activeElement = document?.activeElement
|
|
180
|
+
this.previouslyFocusedElement =
|
|
181
|
+
activeElement && activeElement !== document.body ? activeElement : null
|
|
182
|
+
},
|
|
183
|
+
restoreFocus() {
|
|
184
|
+
const focusTarget = this.previouslyFocusedElement
|
|
185
|
+
this.previouslyFocusedElement = null
|
|
186
|
+
|
|
187
|
+
if (!focusTarget) return
|
|
188
|
+
|
|
189
|
+
this.$nextTick(() => {
|
|
190
|
+
if (
|
|
191
|
+
focusTarget.isConnected &&
|
|
192
|
+
typeof focusTarget.focus === 'function'
|
|
193
|
+
) {
|
|
194
|
+
focusTarget.focus()
|
|
195
|
+
}
|
|
196
|
+
})
|
|
197
|
+
},
|
|
169
198
|
setFocus() {
|
|
170
199
|
this.$nextTick(() => {
|
|
171
200
|
this.focusRef('closeButton')
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<component
|
|
3
3
|
:is="component"
|
|
4
|
-
tabindex="
|
|
4
|
+
:tabindex="resolvedTabindex"
|
|
5
5
|
:class="classes"
|
|
6
|
+
:role="semantic ? 'tab' : undefined"
|
|
7
|
+
:aria-selected="semantic ? String(active) : undefined"
|
|
8
|
+
:aria-controls="semantic && panelId ? panelId : undefined"
|
|
6
9
|
exact
|
|
7
10
|
tag="button"
|
|
8
11
|
:to="to"
|
|
@@ -67,6 +70,8 @@ export default {
|
|
|
67
70
|
badge: { type: Number, default: 0 },
|
|
68
71
|
pushRight: { type: Boolean, default: false },
|
|
69
72
|
active: { type: Boolean, default: false },
|
|
73
|
+
semantic: { type: Boolean, default: false },
|
|
74
|
+
panelId: { type: String, default: '' },
|
|
70
75
|
to: { type: [String, Object], default: '' },
|
|
71
76
|
},
|
|
72
77
|
emits: ['click'],
|
|
@@ -76,6 +81,10 @@ export default {
|
|
|
76
81
|
if (resolveRouter(this) && this.to) return 'router-link'
|
|
77
82
|
return 'button'
|
|
78
83
|
},
|
|
84
|
+
resolvedTabindex() {
|
|
85
|
+
if (!this.semantic) return 0
|
|
86
|
+
return this.active ? 0 : -1
|
|
87
|
+
},
|
|
79
88
|
classes() {
|
|
80
89
|
return {
|
|
81
90
|
'asd20-tab': true,
|
|
@@ -10,8 +10,7 @@
|
|
|
10
10
|
:icon="menuActive ? 'close' : 'menu'"
|
|
11
11
|
:label="menuActive ? 'Close' : 'Menu'"
|
|
12
12
|
:active="menuActive"
|
|
13
|
-
@
|
|
14
|
-
@keyup.enter="toggleMenu"
|
|
13
|
+
@click="toggleMenu"
|
|
15
14
|
/>
|
|
16
15
|
|
|
17
16
|
<slot />
|
|
@@ -89,12 +88,12 @@ export default {
|
|
|
89
88
|
},
|
|
90
89
|
toggleMenu(event) {
|
|
91
90
|
this.$emit('update:menuActive', !this.menuActive)
|
|
92
|
-
this.$emit('keyboardTriggeredLastAction', event
|
|
91
|
+
this.$emit('keyboardTriggeredLastAction', event?.detail === 0)
|
|
93
92
|
this.$emit('update:searchActive', false)
|
|
94
93
|
},
|
|
95
94
|
toggleSearch(event) {
|
|
96
95
|
this.$emit('update:searchActive', !this.searchActive)
|
|
97
|
-
this.$emit('keyboardTriggeredLastAction', event
|
|
96
|
+
this.$emit('keyboardTriggeredLastAction', event?.detail === 0)
|
|
98
97
|
this.$emit('update:menuActive', false)
|
|
99
98
|
},
|
|
100
99
|
},
|
|
@@ -150,6 +149,7 @@ $logo-width: 50vw;
|
|
|
150
149
|
}
|
|
151
150
|
.menu-button,
|
|
152
151
|
.search-button {
|
|
152
|
+
flex-shrink: 0;
|
|
153
153
|
border-radius: var(--website-shape__radius-s);
|
|
154
154
|
color: var(--website-menu__foreground-color);
|
|
155
155
|
& :deep(.asd20-icon g){
|
|
@@ -159,6 +159,12 @@ $logo-width: 50vw;
|
|
|
159
159
|
.emptyNav {
|
|
160
160
|
display: initial;
|
|
161
161
|
}
|
|
162
|
+
|
|
163
|
+
:deep(.asd20-organization-picker),
|
|
164
|
+
:deep(.asd20-organization-picker .asd20-picker),
|
|
165
|
+
:deep(.asd20-organization-picker .asd20-district-logo) {
|
|
166
|
+
min-width: 0;
|
|
167
|
+
}
|
|
162
168
|
}
|
|
163
169
|
|
|
164
170
|
@keyframes navbar-collapse {
|
|
@@ -9,14 +9,18 @@
|
|
|
9
9
|
<div
|
|
10
10
|
ref="scroller"
|
|
11
11
|
class="asd20-tab-bar__scroller"
|
|
12
|
+
:role="semantic ? 'tablist' : undefined"
|
|
12
13
|
:aria-label="ariaLabel"
|
|
13
14
|
@scroll.passive="onScroll"
|
|
14
15
|
>
|
|
15
16
|
<asd20-tab
|
|
16
17
|
v-for="(tab, index) in tabs"
|
|
17
18
|
v-bind="tab"
|
|
19
|
+
ref="tabComponents"
|
|
18
20
|
:key="index"
|
|
21
|
+
:semantic="semantic"
|
|
19
22
|
@click="$emit('tabClick', tab)"
|
|
23
|
+
@keydown="onTabKeydown($event, index)"
|
|
20
24
|
/>
|
|
21
25
|
<slot />
|
|
22
26
|
</div>
|
|
@@ -41,8 +45,11 @@ export default {
|
|
|
41
45
|
props: {
|
|
42
46
|
tabs: { type: Array, default: () => [] },
|
|
43
47
|
ariaLabel: { type: String, default: 'Tab List' },
|
|
48
|
+
semantic: { type: Boolean, default: false },
|
|
44
49
|
},
|
|
45
50
|
|
|
51
|
+
emits: ['tabClick'],
|
|
52
|
+
|
|
46
53
|
data: () => ({
|
|
47
54
|
canScrollLeft: false,
|
|
48
55
|
canScrollRight: false,
|
|
@@ -75,6 +82,39 @@ export default {
|
|
|
75
82
|
},
|
|
76
83
|
|
|
77
84
|
methods: {
|
|
85
|
+
onTabKeydown(event, index) {
|
|
86
|
+
if (!this.semantic || !this.tabs.length) return
|
|
87
|
+
|
|
88
|
+
const lastIndex = this.tabs.length - 1
|
|
89
|
+
const destinations = {
|
|
90
|
+
ArrowLeft: index === 0 ? lastIndex : index - 1,
|
|
91
|
+
ArrowRight: index === lastIndex ? 0 : index + 1,
|
|
92
|
+
Home: 0,
|
|
93
|
+
End: lastIndex,
|
|
94
|
+
}
|
|
95
|
+
const destination = destinations[event.key]
|
|
96
|
+
|
|
97
|
+
if (destination === undefined) return
|
|
98
|
+
|
|
99
|
+
event.preventDefault()
|
|
100
|
+
this.focusAndActivateTab(destination)
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
focusAndActivateTab(index) {
|
|
104
|
+
const tabComponents = Array.isArray(this.$refs.tabComponents)
|
|
105
|
+
? this.$refs.tabComponents
|
|
106
|
+
: []
|
|
107
|
+
const tabComponent = tabComponents[index]
|
|
108
|
+
const tabElement = tabComponent?.$el || tabComponent
|
|
109
|
+
|
|
110
|
+
if (typeof tabElement?.focus === 'function') {
|
|
111
|
+
tabElement.focus()
|
|
112
|
+
}
|
|
113
|
+
if (typeof tabElement?.click === 'function') {
|
|
114
|
+
tabElement.click()
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
|
|
78
118
|
onScroll() {
|
|
79
119
|
this.updateScrollState()
|
|
80
120
|
},
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<component
|
|
3
3
|
:is="tag"
|
|
4
|
+
id="main"
|
|
5
|
+
tabindex="-1"
|
|
4
6
|
class="asd20-video-header-content"
|
|
5
7
|
>
|
|
6
8
|
<div class="asd20-video-header-content__shell">
|
|
@@ -10,7 +12,6 @@
|
|
|
10
12
|
<section class="header-messaging-section">
|
|
11
13
|
<asd20-messaging
|
|
12
14
|
v-bind="messages[0]"
|
|
13
|
-
id="main"
|
|
14
15
|
heading-tag-name="h1"
|
|
15
16
|
></asd20-messaging>
|
|
16
17
|
</section>
|
|
@@ -49,86 +49,95 @@
|
|
|
49
49
|
|
|
50
50
|
<!-- Page Content -->
|
|
51
51
|
|
|
52
|
-
<
|
|
53
|
-
|
|
54
|
-
<
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
/>
|
|
70
|
-
<asd20-list
|
|
71
|
-
class="logins-list"
|
|
72
|
-
multi-column
|
|
73
|
-
:column-width="480"
|
|
52
|
+
<main class="asd20-template-logins__main">
|
|
53
|
+
<slot></slot>
|
|
54
|
+
<section aria-label="Login Tabs">
|
|
55
|
+
<asd20-tab-bar
|
|
56
|
+
:tabs="tabsList"
|
|
57
|
+
aria-label="Login categories"
|
|
58
|
+
class="logins-tab-bar"
|
|
59
|
+
semantic
|
|
60
|
+
@tabClick="onTabClick"
|
|
61
|
+
/>
|
|
62
|
+
<asd20-viewport>
|
|
63
|
+
<!-- selectedGroup -->
|
|
64
|
+
<section
|
|
65
|
+
v-if="selectedTabLogins"
|
|
66
|
+
:id="selectedTabLogins.panelId"
|
|
67
|
+
role="tabpanel"
|
|
68
|
+
:aria-labelledby="selectedTabLogins.tabId"
|
|
74
69
|
>
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
:
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
70
|
+
<!-- Inline Notifications -->
|
|
71
|
+
<asd20-notification-group
|
|
72
|
+
class="asd20-notification-group--inline"
|
|
73
|
+
:notifications="activeNotificationsByType.inline"
|
|
74
|
+
group-type="inline"
|
|
75
|
+
@dismiss="$emit('dismiss-notification', $event)"
|
|
76
|
+
@toggle-all="$emit('toggle-all')"
|
|
77
|
+
/>
|
|
78
|
+
<asd20-list
|
|
79
|
+
class="logins-list"
|
|
80
|
+
multi-column
|
|
81
|
+
:column-width="480"
|
|
82
82
|
>
|
|
83
|
-
<
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
83
|
+
<asd20-list-item
|
|
84
|
+
v-for="(login, index) in selectedTabLogins.logins"
|
|
85
|
+
:key="index"
|
|
86
|
+
:label="login.title"
|
|
87
|
+
class="login-list-item"
|
|
88
|
+
align-top
|
|
89
|
+
bordered
|
|
90
|
+
>
|
|
91
|
+
<template #actions>
|
|
92
|
+
<asd20-button
|
|
93
|
+
target="_blank"
|
|
94
|
+
horizontal
|
|
95
|
+
reversed
|
|
96
|
+
:label="
|
|
97
|
+
login.callsToAction && login.callsToAction[0].label
|
|
98
|
+
? login.callsToAction[0].label
|
|
99
|
+
: 'Login'
|
|
100
|
+
"
|
|
101
|
+
:aria-label="`Login to ${login.title}`"
|
|
102
|
+
:link="login.callsToAction[0].url || ''"
|
|
103
|
+
/>
|
|
104
|
+
</template>
|
|
105
|
+
<div class="asd20-list-item__description">
|
|
106
|
+
{{ login.summary }}
|
|
107
|
+
<a
|
|
108
|
+
v-if="login.bodyHtml"
|
|
109
|
+
href="#"
|
|
110
|
+
@click.prevent="selectedLogin = login"
|
|
111
|
+
>
|
|
112
|
+
{{ login.title }} details.
|
|
113
|
+
</a>
|
|
114
|
+
</div>
|
|
115
|
+
</asd20-list-item>
|
|
116
|
+
</asd20-list>
|
|
117
|
+
</section>
|
|
118
|
+
</asd20-viewport>
|
|
119
|
+
</section>
|
|
120
|
+
|
|
121
|
+
<asd20-modal
|
|
122
|
+
icon="login"
|
|
123
|
+
title="Login / Application Details"
|
|
124
|
+
windowed
|
|
125
|
+
dismissable
|
|
126
|
+
:open="selectedLogin != null"
|
|
127
|
+
class="asd20-login-details-modal"
|
|
128
|
+
@dismiss="selectedLogin = null"
|
|
126
129
|
>
|
|
127
|
-
<asd20-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
130
|
+
<asd20-viewport
|
|
131
|
+
v-if="selectedLogin"
|
|
132
|
+
padded
|
|
133
|
+
scrollable
|
|
134
|
+
>
|
|
135
|
+
<asd20-messaging-group
|
|
136
|
+
:messages="[selectedLogin]"
|
|
137
|
+
></asd20-messaging-group>
|
|
138
|
+
</asd20-viewport>
|
|
139
|
+
</asd20-modal>
|
|
140
|
+
</main>
|
|
132
141
|
<!-- </asd20-page-content> -->
|
|
133
142
|
|
|
134
143
|
<slot name="before-footer" />
|
|
@@ -228,12 +237,18 @@ export default {
|
|
|
228
237
|
}
|
|
229
238
|
)
|
|
230
239
|
|
|
231
|
-
return sortedCategories.map(c =>
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
240
|
+
return sortedCategories.map(c => {
|
|
241
|
+
const tabId = `tab-${kebabCase(c)}`
|
|
242
|
+
const hash = `#${tabId}`
|
|
243
|
+
|
|
244
|
+
return {
|
|
245
|
+
id: tabId,
|
|
246
|
+
panelId: `${tabId}-panel`,
|
|
247
|
+
label: c,
|
|
248
|
+
hash,
|
|
249
|
+
active: this.tab === hash,
|
|
250
|
+
}
|
|
251
|
+
})
|
|
237
252
|
},
|
|
238
253
|
logins() {
|
|
239
254
|
return (this.messages || []).map(m => {
|
|
@@ -263,6 +278,8 @@ export default {
|
|
|
263
278
|
|
|
264
279
|
groups.push({
|
|
265
280
|
tab: t.label,
|
|
281
|
+
tabId: t.id,
|
|
282
|
+
panelId: t.panelId,
|
|
266
283
|
hash: t.hash,
|
|
267
284
|
logins: loginsForTab,
|
|
268
285
|
})
|
|
@@ -8,6 +8,7 @@ export default {
|
|
|
8
8
|
open: { type: Boolean, default: undefined },
|
|
9
9
|
initialFocus: { type: null, default: undefined },
|
|
10
10
|
allowOutsideClick: { type: [Boolean, Function], default: undefined },
|
|
11
|
+
returnFocusOnDeactivate: { type: Boolean, default: true },
|
|
11
12
|
},
|
|
12
13
|
data: () => ({
|
|
13
14
|
trap: null,
|
|
@@ -24,6 +25,7 @@ export default {
|
|
|
24
25
|
clickOutsideDeactivates: false,
|
|
25
26
|
escapeDeactivates: false,
|
|
26
27
|
fallbackFocus: () => this.ensureFallbackFocusTarget(),
|
|
28
|
+
returnFocusOnDeactivate: this.returnFocusOnDeactivate,
|
|
27
29
|
}
|
|
28
30
|
|
|
29
31
|
if (this.initialFocus !== undefined) {
|