@asd20/ui-next 2.21.11 → 2.22.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.22.0](https://github.com/academydistrict20/asd20-ui-next/compare/ui-next-v2.21.11...ui-next-v2.22.0) (2026-08-28)
4
+
5
+
6
+ ### Features
7
+
8
+ * accessibility improvements for directory; close modal with esc; focus changes ([8e29660](https://github.com/academydistrict20/asd20-ui-next/commit/8e2966030e00f4165e5622ecb60c80799649b2fb))
9
+
3
10
  ## [2.21.11](https://github.com/academydistrict20/asd20-ui-next/compare/ui-next-v2.21.10...ui-next-v2.21.11) (2026-08-27)
4
11
 
5
12
  ## [2.21.10](https://github.com/academydistrict20/asd20-ui-next/compare/ui-next-v2.21.9...ui-next-v2.21.10) (2026-08-27)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@asd20/ui-next",
3
- "version": "2.21.11",
3
+ "version": "2.22.0",
4
4
  "private": false,
5
5
  "description": "ASD20 UI component library for Vue 3.",
6
6
  "license": "MIT",
@@ -34,6 +34,7 @@
34
34
  <asd20-button
35
35
  :aria-labelledby="headingId"
36
36
  :aria-controls="contentId"
37
+ :aria-expanded="open"
37
38
  :icon-angle="open ? 180 : 90"
38
39
  icon="chevron"
39
40
  size="sm"
@@ -45,7 +46,6 @@
45
46
  <div
46
47
  :id="contentId"
47
48
  ref="content"
48
- :aria-expanded="open"
49
49
  :aria-hidden="open ? 'false' : 'true'"
50
50
  :inert="open ? null : ''"
51
51
  :class="{
@@ -9,12 +9,18 @@
9
9
  >
10
10
  <div
11
11
  v-if="resolvedOpen"
12
+ ref="container"
12
13
  :class="classes"
13
14
  >
14
15
  <focus-trap
15
16
  :open="resolvedOpen"
16
17
  >
17
- <div class="asd20-modal">
18
+ <div
19
+ class="asd20-modal"
20
+ role="dialog"
21
+ aria-modal="true"
22
+ :aria-labelledby="hasTitle ? titleId : undefined"
23
+ >
18
24
  <div
19
25
  v-if="title || $slots.title"
20
26
  class="asd20-modal__header"
@@ -24,7 +30,9 @@
24
30
  :name="icon"
25
31
  size="md"
26
32
  />
27
- <h2>{{ title }}<slot name="title" /></h2>
33
+ <h2 :id="titleId">
34
+ {{ title }}<slot name="title" />
35
+ </h2>
28
36
 
29
37
  <asd20-button
30
38
  v-if="dismissable"
@@ -58,6 +66,7 @@
58
66
  import Asd20Icon from '../../atoms/Asd20Icon'
59
67
  import Asd20Button from '../../atoms/Asd20Button'
60
68
  import FocusTrap from '../../utils/FocusTrap'
69
+ import createComponentInstanceId from '../../../utils/createComponentInstanceId'
61
70
 
62
71
  export default {
63
72
  name: 'Asd20Modal',
@@ -70,9 +79,13 @@ export default {
70
79
  dismissable: { type: Boolean, default: false },
71
80
  windowed: { type: Boolean, default: false },
72
81
  },
73
- data: () => ({
74
- transitioning: false,
75
- }),
82
+ data() {
83
+ return {
84
+ listeningForEscape: false,
85
+ transitioning: false,
86
+ titleId: createComponentInstanceId('asd20-modal-title'),
87
+ }
88
+ },
76
89
  computed: {
77
90
  resolvedOpen() {
78
91
  return typeof this.modelValue === 'boolean'
@@ -85,26 +98,67 @@ export default {
85
98
  'asd20-modal-container--windowed': this.windowed,
86
99
  }
87
100
  },
101
+ hasTitle() {
102
+ return Boolean(this.title || this.$slots.title)
103
+ },
88
104
  },
89
105
  watch: {
90
106
  resolvedOpen: function(newVal) {
91
107
  if (newVal) {
108
+ this.startListeningForEscape()
92
109
  this.setFocus()
110
+ return
93
111
  }
112
+
113
+ this.stopListeningForEscape()
94
114
  },
95
115
  },
96
116
 
97
117
  mounted() {
98
118
  if (this.resolvedOpen === true) {
119
+ this.startListeningForEscape()
99
120
  this.setFocus()
100
121
  }
101
122
  },
123
+ beforeUnmount() {
124
+ this.stopListeningForEscape()
125
+ },
102
126
  methods: {
103
127
  dismiss(value = false) {
104
128
  this.$emit('dismiss', value)
105
129
  this.$emit('update:open', value)
106
130
  this.$emit('update:modelValue', value)
107
131
  },
132
+ dismissOnEscape(event) {
133
+ const openModals = document.querySelectorAll('.asd20-modal-container')
134
+ const topmostModal = openModals[openModals.length - 1]
135
+ const currentModal = this.$refs.container
136
+
137
+ if (
138
+ event.key !== 'Escape' ||
139
+ !this.dismissable ||
140
+ event.defaultPrevented ||
141
+ topmostModal !== currentModal
142
+ ) {
143
+ return
144
+ }
145
+
146
+ event.preventDefault()
147
+ event.stopImmediatePropagation()
148
+ this.dismiss(false)
149
+ },
150
+ startListeningForEscape() {
151
+ if (this.listeningForEscape) return
152
+
153
+ document.addEventListener('keydown', this.dismissOnEscape)
154
+ this.listeningForEscape = true
155
+ },
156
+ stopListeningForEscape() {
157
+ if (!this.listeningForEscape) return
158
+
159
+ document.removeEventListener('keydown', this.dismissOnEscape)
160
+ this.listeningForEscape = false
161
+ },
108
162
  focusRef(refName) {
109
163
  const ref = this.$refs[refName]
110
164
  const focusTarget = ref?.$el || ref
@@ -106,9 +106,9 @@ export default {
106
106
  transition: color 0.2s ease-out, border-color 0.2s ease-out,
107
107
  opacity 0.2s ease-out;
108
108
  font-weight: bold;
109
- color: var(--color__primary-t50);
109
+ color: var(--color__primary-t40);
110
110
  text-decoration: none;
111
- opacity: 0.8;
111
+ opacity: 1;
112
112
 
113
113
  &::after {
114
114
  content: '';
@@ -136,19 +136,29 @@ export default {
136
136
  }
137
137
 
138
138
  &:focus {
139
- outline: none !important;
139
+ outline: 2px solid var(--color__accent) !important;
140
+ outline-offset: -2px;
140
141
  box-shadow: none !important;
141
142
  }
142
143
 
143
144
  &:focus-visible {
144
- outline: none !important;
145
+ outline: 2px solid var(--color__accent) !important;
146
+ outline-offset: -2px;
145
147
  box-shadow: none !important;
146
148
  }
147
149
 
150
+ &:focus:not(:focus-visible) {
151
+ outline: none !important;
152
+ }
153
+
148
154
  &:focus::after,
149
155
  &:focus-visible::after {
150
- background: var(--color__accent-t60);
151
- box-shadow: 0 0 0 2px var(--color__accent-t80);
156
+ background: var(--color__accent);
157
+ box-shadow: none;
158
+ }
159
+
160
+ &:focus:not(:focus-visible)::after {
161
+ background: transparent;
152
162
  }
153
163
 
154
164
  &--active {
@@ -157,20 +167,9 @@ export default {
157
167
  &::after {
158
168
  background: var(--color__accent);
159
169
  }
160
- &:focus {
161
- outline: none !important;
162
- box-shadow: none !important;
163
- }
164
-
165
- &:focus-visible {
166
- outline: none !important;
167
- box-shadow: none !important;
168
- }
169
170
 
170
- &:focus::after,
171
- &:focus-visible::after {
171
+ &:focus:not(:focus-visible)::after {
172
172
  background: var(--color__accent);
173
- box-shadow: 0 0 0 2px var(--color__accent-t70);
174
173
  }
175
174
  }
176
175