@asd20/ui-next 2.23.5 → 2.23.6
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.23.6](https://github.com/academydistrict20/asd20-ui-next/compare/ui-next-v2.23.5...ui-next-v2.23.6) (2026-09-16)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* restore/harden accessibility issue form ([d7ef90c](https://github.com/academydistrict20/asd20-ui-next/commit/d7ef90c76f1675e53528374cf226d05d38af1929))
|
|
9
|
+
|
|
3
10
|
## [2.23.5](https://github.com/academydistrict20/asd20-ui-next/compare/ui-next-v2.23.4...ui-next-v2.23.5) (2026-09-11)
|
|
4
11
|
|
|
5
12
|
|
package/package.json
CHANGED
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
<div
|
|
12
12
|
v-if="submissionSuccess"
|
|
13
13
|
class="submission-success"
|
|
14
|
+
role="status"
|
|
15
|
+
aria-live="polite"
|
|
14
16
|
>
|
|
15
17
|
<p style="text-align: center; font-weight: bold; font-size: 1.1rem;">
|
|
16
18
|
Thank you! Your issue has been submitted.
|
|
@@ -60,6 +62,7 @@
|
|
|
60
62
|
id="issue"
|
|
61
63
|
v-model="form.issue"
|
|
62
64
|
label="Describe the Issue"
|
|
65
|
+
maxlength="2000"
|
|
63
66
|
help-text="<ul style='margin: 0 0 0.5rem 1rem; font-size: 0.75rem !important;'>
|
|
64
67
|
<li>Specify the exact URL, program, service, or digital asset you had a problem with.</li>
|
|
65
68
|
<li>Provide as much detail as possible, including the nature of the accessibility barrier, the date and time it occurred, any assistive technology you were using, or the nature of the program or service being denied.</li>
|
|
@@ -69,18 +72,55 @@
|
|
|
69
72
|
:validator="validateIssue"
|
|
70
73
|
@validated="validationErrors.issue = $event"
|
|
71
74
|
/>
|
|
72
|
-
<
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
75
|
+
<div
|
|
76
|
+
class="asd20-accessibility-issue-modal__honeypot"
|
|
77
|
+
aria-hidden="true"
|
|
78
|
+
>
|
|
79
|
+
<label for="accessibility-website">Website</label>
|
|
80
|
+
<input
|
|
81
|
+
id="accessibility-website"
|
|
82
|
+
v-model="form.website"
|
|
83
|
+
type="text"
|
|
84
|
+
name="website"
|
|
85
|
+
tabindex="-1"
|
|
86
|
+
autocomplete="off"
|
|
87
|
+
>
|
|
88
|
+
</div>
|
|
89
|
+
<div
|
|
90
|
+
class="asd20-accessibility-issue-modal__submit"
|
|
91
|
+
aria-live="polite"
|
|
92
|
+
>
|
|
93
|
+
<Recaptcha
|
|
94
|
+
v-if="!sending"
|
|
95
|
+
sitekey="6LfidKoUAAAAAFqr3QEbia3jIkecsZyxBYlMvWrX"
|
|
96
|
+
:load-recaptcha-script="true"
|
|
97
|
+
size="invisible"
|
|
98
|
+
@verify="captchaVerified"
|
|
99
|
+
@error="captchaFailed"
|
|
100
|
+
>
|
|
101
|
+
<asd20-button
|
|
102
|
+
label="Submit Issue"
|
|
103
|
+
:disabled="!isValid || sending"
|
|
104
|
+
horizontal
|
|
105
|
+
centered
|
|
106
|
+
bordered
|
|
107
|
+
@click="validateBeforeSend"
|
|
108
|
+
/>
|
|
109
|
+
</Recaptcha>
|
|
110
|
+
<asd20-spinner
|
|
111
|
+
v-else
|
|
112
|
+
size="sm"
|
|
113
|
+
/>
|
|
114
|
+
</div>
|
|
115
|
+
<div
|
|
116
|
+
v-if="feedbackMessage"
|
|
117
|
+
ref="feedback"
|
|
118
|
+
class="asd20-accessibility-issue-modal__feedback"
|
|
119
|
+
:role="feedbackRole"
|
|
120
|
+
tabindex="-1"
|
|
121
|
+
>
|
|
122
|
+
{{ feedbackMessage }}
|
|
123
|
+
</div>
|
|
84
124
|
</template>
|
|
85
125
|
</Asd20Viewport>
|
|
86
126
|
</asd20-modal>
|
|
@@ -93,8 +133,10 @@ import Asd20TextAreaInput from '../../molecules/Asd20TextAreaInput'
|
|
|
93
133
|
import Asd20Viewport from '../../atoms/Asd20Viewport'
|
|
94
134
|
import Asd20Spinner from '../../atoms/Asd20Spinner'
|
|
95
135
|
import Asd20Button from '../../atoms/Asd20Button'
|
|
96
|
-
|
|
97
|
-
|
|
136
|
+
import Recaptcha from '../../utils/Recaptcha'
|
|
137
|
+
import sendAccessibilityIssue, {
|
|
138
|
+
resolveAccessibilityIssueEndpoint,
|
|
139
|
+
} from '../../../helpers/sendAccessibilityIssue.js'
|
|
98
140
|
|
|
99
141
|
export default {
|
|
100
142
|
name: 'Asd20AccessibilityIssueModal',
|
|
@@ -105,6 +147,7 @@ export default {
|
|
|
105
147
|
Asd20Viewport,
|
|
106
148
|
Asd20Spinner,
|
|
107
149
|
Asd20Button,
|
|
150
|
+
Recaptcha,
|
|
108
151
|
},
|
|
109
152
|
props: {
|
|
110
153
|
open: { type: Boolean, default: false },
|
|
@@ -113,6 +156,8 @@ export default {
|
|
|
113
156
|
return {
|
|
114
157
|
sending: false,
|
|
115
158
|
submissionSuccess: false,
|
|
159
|
+
feedbackMessage: '',
|
|
160
|
+
feedbackRole: 'alert',
|
|
116
161
|
form: {
|
|
117
162
|
firstName: '',
|
|
118
163
|
lastName: '',
|
|
@@ -121,6 +166,7 @@ export default {
|
|
|
121
166
|
address: '',
|
|
122
167
|
organization: '',
|
|
123
168
|
issue: '',
|
|
169
|
+
website: '',
|
|
124
170
|
},
|
|
125
171
|
validationErrors: {
|
|
126
172
|
firstName: [],
|
|
@@ -135,13 +181,20 @@ export default {
|
|
|
135
181
|
return Object.values(this.validationErrors).every(e => e.length === 0)
|
|
136
182
|
},
|
|
137
183
|
},
|
|
184
|
+
watch: {
|
|
185
|
+
open(value) {
|
|
186
|
+
if (value) {
|
|
187
|
+
this.submissionSuccess = false
|
|
188
|
+
this.feedbackMessage = ''
|
|
189
|
+
}
|
|
190
|
+
},
|
|
191
|
+
},
|
|
138
192
|
methods: {
|
|
139
193
|
validateEmailAddress({ value, validationErrors }) {
|
|
140
194
|
const pattern = /^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/
|
|
141
195
|
if (!pattern.test(value)) {
|
|
142
196
|
validationErrors.push('A valid email address is required')
|
|
143
197
|
}
|
|
144
|
-
console.log('Email validation errors:', validationErrors)
|
|
145
198
|
return validationErrors
|
|
146
199
|
},
|
|
147
200
|
validateIssue({ value, validationErrors }) {
|
|
@@ -155,25 +208,116 @@ export default {
|
|
|
155
208
|
}
|
|
156
209
|
return validationErrors
|
|
157
210
|
},
|
|
158
|
-
|
|
159
|
-
|
|
211
|
+
resolveRuntimeConfig() {
|
|
212
|
+
return (
|
|
213
|
+
this.$config ||
|
|
214
|
+
this.$?.appContext?.config?.globalProperties?.$config ||
|
|
215
|
+
{}
|
|
216
|
+
)
|
|
217
|
+
},
|
|
218
|
+
getFormValidationMessages() {
|
|
219
|
+
const messages = []
|
|
220
|
+
const emailPattern =
|
|
221
|
+
/^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/
|
|
222
|
+
|
|
223
|
+
if (!String(this.form.firstName || '').trim())
|
|
224
|
+
messages.push('First name is required.')
|
|
225
|
+
if (!String(this.form.lastName || '').trim())
|
|
226
|
+
messages.push('Last name is required.')
|
|
227
|
+
if (!emailPattern.test(String(this.form.email || '').trim()))
|
|
228
|
+
messages.push('Enter a valid email address.')
|
|
229
|
+
|
|
230
|
+
const issue = String(this.form.issue || '').trim()
|
|
231
|
+
if (!issue) messages.push('Describe the accessibility issue.')
|
|
232
|
+
else if (issue.length > 2000)
|
|
233
|
+
messages.push('The issue description must be 2,000 characters or fewer.')
|
|
234
|
+
|
|
235
|
+
return messages
|
|
236
|
+
},
|
|
237
|
+
validateBeforeSend(event) {
|
|
238
|
+
const messages = this.getFormValidationMessages()
|
|
239
|
+
if (!messages.length) return
|
|
240
|
+
|
|
241
|
+
event?.preventDefault?.()
|
|
242
|
+
this.showFeedback(messages.join(' '))
|
|
243
|
+
},
|
|
244
|
+
captchaVerified(token) {
|
|
245
|
+
if (token) this.sendAccessibilityIssue(token)
|
|
246
|
+
},
|
|
247
|
+
captchaFailed() {
|
|
248
|
+
this.showFeedback(
|
|
249
|
+
'The verification service could not start. Please try again or use the email and phone options on this page.'
|
|
250
|
+
)
|
|
251
|
+
},
|
|
252
|
+
showFeedback(message, role = 'alert') {
|
|
253
|
+
this.feedbackMessage = message
|
|
254
|
+
this.feedbackRole = role
|
|
255
|
+
this.$nextTick(() => this.$refs.feedback?.focus?.())
|
|
256
|
+
},
|
|
257
|
+
getServerFailureMessage(error) {
|
|
258
|
+
const status = Number(error?.response?.status || 0)
|
|
259
|
+
const serverMessage = error?.response?.data?.error
|
|
260
|
+
|
|
261
|
+
if (status === 400 && serverMessage) return serverMessage
|
|
262
|
+
if (status === 403)
|
|
263
|
+
return 'The verification was not accepted. Please try again.'
|
|
264
|
+
if (status === 429)
|
|
265
|
+
return 'Too many reports were submitted recently. Please wait and try again, or use the email and phone options on this page.'
|
|
266
|
+
|
|
267
|
+
return 'Something went wrong while submitting the form. Please try again later or use the email and phone options on this page.'
|
|
268
|
+
},
|
|
269
|
+
async sendAccessibilityIssue(captchaToken = '') {
|
|
270
|
+
const validationMessages = this.getFormValidationMessages()
|
|
271
|
+
if (validationMessages.length) {
|
|
272
|
+
this.showFeedback(validationMessages.join(' '))
|
|
273
|
+
return
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const endpoint = resolveAccessibilityIssueEndpoint({
|
|
277
|
+
runtimeConfig: this.resolveRuntimeConfig(),
|
|
278
|
+
})
|
|
279
|
+
const submissionClient =
|
|
280
|
+
this.$sendAccessibilityIssue ||
|
|
281
|
+
(endpoint
|
|
282
|
+
? async issue => await sendAccessibilityIssue(issue, endpoint)
|
|
283
|
+
: null)
|
|
284
|
+
|
|
285
|
+
if (!submissionClient) {
|
|
286
|
+
console.error('Accessibility issue endpoint is not configured.')
|
|
287
|
+
this.showFeedback(
|
|
288
|
+
'Issue reporting is not configured for this environment. Please use the email and phone options on this page.'
|
|
289
|
+
)
|
|
290
|
+
return
|
|
291
|
+
}
|
|
292
|
+
|
|
160
293
|
this.sending = true
|
|
294
|
+
this.feedbackMessage = ''
|
|
161
295
|
|
|
162
296
|
try {
|
|
163
|
-
|
|
164
|
-
|
|
297
|
+
await submissionClient({
|
|
298
|
+
...this.form,
|
|
299
|
+
captchaToken,
|
|
300
|
+
})
|
|
165
301
|
this.submissionSuccess = true
|
|
166
302
|
|
|
167
303
|
// Wait 2 seconds, then close the modal
|
|
168
304
|
setTimeout(() => {
|
|
169
305
|
this.submissionSuccess = false
|
|
306
|
+
this.form = {
|
|
307
|
+
firstName: '',
|
|
308
|
+
lastName: '',
|
|
309
|
+
email: '',
|
|
310
|
+
phone: '',
|
|
311
|
+
address: '',
|
|
312
|
+
organization: '',
|
|
313
|
+
issue: '',
|
|
314
|
+
website: '',
|
|
315
|
+
}
|
|
170
316
|
this.$emit('dismiss')
|
|
171
317
|
}, 2000)
|
|
172
318
|
} catch (e) {
|
|
173
319
|
console.error('Accessibility issue submission failed:', e.message || e)
|
|
174
|
-
|
|
175
|
-
'Something went wrong while submitting the form. Please try again later.'
|
|
176
|
-
)
|
|
320
|
+
this.showFeedback(this.getServerFailureMessage(e))
|
|
177
321
|
} finally {
|
|
178
322
|
this.sending = false
|
|
179
323
|
}
|
|
@@ -193,6 +337,27 @@ export default {
|
|
|
193
337
|
font-family: Arial, sans-serif;
|
|
194
338
|
font-size: 1rem;
|
|
195
339
|
}
|
|
340
|
+
|
|
341
|
+
&__submit {
|
|
342
|
+
display: flex;
|
|
343
|
+
justify-content: center;
|
|
344
|
+
min-height: 3rem;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
&__feedback {
|
|
348
|
+
font-weight: 600;
|
|
349
|
+
margin: space(0.5) 0;
|
|
350
|
+
text-align: center;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
&__honeypot {
|
|
354
|
+
position: absolute;
|
|
355
|
+
width: 1px;
|
|
356
|
+
height: 1px;
|
|
357
|
+
margin: -1px;
|
|
358
|
+
overflow: hidden;
|
|
359
|
+
clip: rect(0, 0, 0, 0);
|
|
360
|
+
}
|
|
196
361
|
}
|
|
197
362
|
|
|
198
363
|
@media (min-width: 1024px) {
|
|
@@ -1,71 +1,64 @@
|
|
|
1
1
|
import axios from 'axios'
|
|
2
2
|
|
|
3
|
-
import { resolveRuntimeConfigValue } from './runtimeConfig'
|
|
3
|
+
import { normalizeBaseUrl, resolveRuntimeConfigValue } from './runtimeConfig'
|
|
4
|
+
|
|
5
|
+
const ACCESSIBILITY_ISSUE_ROUTE = '/api/accessibility/issues'
|
|
4
6
|
|
|
5
7
|
function issueIsValid(issue) {
|
|
6
|
-
return
|
|
8
|
+
return Boolean(
|
|
9
|
+
issue &&
|
|
10
|
+
String(issue.firstName || '').trim() &&
|
|
11
|
+
String(issue.lastName || '').trim() &&
|
|
12
|
+
String(issue.email || '').trim() &&
|
|
13
|
+
String(issue.issue || '').trim()
|
|
14
|
+
)
|
|
7
15
|
}
|
|
8
16
|
|
|
9
|
-
export
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
}
|
|
17
|
+
export function resolveAccessibilityIssueEndpoint({
|
|
18
|
+
endpoint = null,
|
|
19
|
+
runtimeConfig = {},
|
|
20
|
+
} = {}) {
|
|
21
|
+
const explicitEndpoint = resolveRuntimeConfigValue({
|
|
22
|
+
explicitValue: endpoint,
|
|
23
|
+
runtimeConfig,
|
|
24
|
+
runtimeConfigKeys: ['accessibilityIssueEndpoint'],
|
|
25
|
+
envKeys: ['ACCESSIBILITY_ISSUE_ENDPOINT'],
|
|
26
|
+
})
|
|
27
|
+
if (explicitEndpoint) return String(explicitEndpoint).trim()
|
|
19
28
|
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
categoryId: options,
|
|
25
|
-
}
|
|
26
|
-
const categoryId = resolveRuntimeConfigValue({
|
|
27
|
-
explicitValue: resolvedOptions.categoryId,
|
|
28
|
-
runtimeConfig: resolvedOptions.runtimeConfig,
|
|
29
|
-
runtimeConfigKeys: ['happyFoxCategoryId'],
|
|
30
|
-
envKeys: ['HAPPYFOX_CATEGORY_ID'],
|
|
29
|
+
const functionsEndpoint = resolveRuntimeConfigValue({
|
|
30
|
+
runtimeConfig,
|
|
31
|
+
runtimeConfigKeys: ['functionsEndpoint'],
|
|
32
|
+
envKeys: ['FUNCTIONS_ENDPOINT'],
|
|
31
33
|
})
|
|
32
|
-
|
|
33
|
-
|
|
34
|
+
const baseUrl = normalizeBaseUrl(functionsEndpoint)
|
|
35
|
+
|
|
36
|
+
return baseUrl ? `${baseUrl}${ACCESSIBILITY_ISSUE_ROUTE}` : null
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export default async function sendAccessibilityIssue(issue, endpoint) {
|
|
40
|
+
if (!issueIsValid(issue)) {
|
|
41
|
+
throw new Error('The issue object is incomplete or invalid.')
|
|
34
42
|
}
|
|
35
43
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
email: issue.email,
|
|
39
|
-
phone: issue.phone || '',
|
|
40
|
-
subject: 'Accessibility Issue Reported via External ASD20 Website',
|
|
41
|
-
html: `
|
|
42
|
-
<p><strong>Issue Details:</strong></p>
|
|
43
|
-
<p>${issue.issue}</p>
|
|
44
|
-
<p><strong>Mailing Address:</strong> ${issue.address || 'Not provided'}</p>
|
|
45
|
-
<p><strong>Organization:</strong> ${issue.organization || 'Not provided'}</p>
|
|
46
|
-
`,
|
|
47
|
-
category: Number(categoryId),
|
|
44
|
+
if (!endpoint) {
|
|
45
|
+
throw new Error('The accessibility issue endpoint is not configured.')
|
|
48
46
|
}
|
|
49
47
|
|
|
50
48
|
try {
|
|
51
|
-
const response = await axios.post(endpoint,
|
|
52
|
-
headers: {
|
|
53
|
-
Authorization: `Basic ${btoa(`${apiKey}:${authCode}`)}`,
|
|
54
|
-
'Content-Type': 'application/json',
|
|
55
|
-
Accept: 'application/json'
|
|
56
|
-
}
|
|
57
|
-
})
|
|
58
|
-
|
|
49
|
+
const response = await axios.post(endpoint, issue)
|
|
59
50
|
return response.data
|
|
60
51
|
} catch (error) {
|
|
61
52
|
if (error.response) {
|
|
62
|
-
|
|
63
|
-
`
|
|
53
|
+
const submissionError = new Error(
|
|
54
|
+
`Accessibility submission error ${error.response.status}: ${JSON.stringify(
|
|
64
55
|
error.response.data
|
|
65
56
|
)}`
|
|
66
57
|
)
|
|
67
|
-
|
|
68
|
-
throw
|
|
58
|
+
submissionError.response = error.response
|
|
59
|
+
throw submissionError
|
|
69
60
|
}
|
|
61
|
+
|
|
62
|
+
throw new Error(`Accessibility submission request error: ${error.message}`)
|
|
70
63
|
}
|
|
71
64
|
}
|