@asd20/ui-next 2.24.3 → 2.25.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.25.0](https://github.com/academydistrict20/asd20-ui-next/compare/ui-next-v2.24.4...ui-next-v2.25.0) (2026-09-21)
4
+
5
+
6
+ ### Features
7
+
8
+ * support public form date limits and fallback guidance ([4ef8551](https://github.com/academydistrict20/asd20-ui-next/commit/4ef8551aaa98cf66f48395a9bf98d68f107a0486))
9
+
10
+ ## [2.24.4](https://github.com/academydistrict20/asd20-ui-next/compare/ui-next-v2.24.3...ui-next-v2.24.4) (2026-09-17)
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * remove legacy accessibility form templates and components ([3e9fa07](https://github.com/academydistrict20/asd20-ui-next/commit/3e9fa0749a9000f9c8970bbb8d1a81ed665a94e8))
16
+
3
17
  ## [2.24.3](https://github.com/academydistrict20/asd20-ui-next/compare/ui-next-v2.24.2...ui-next-v2.24.3) (2026-09-17)
4
18
 
5
19
 
package/components.js CHANGED
@@ -66,7 +66,6 @@ export { default as Asd20Toolbar } from './src/components/molecules/Asd20Toolbar
66
66
  export { default as Asd20WidgetGroup } from './src/components/molecules/Asd20WidgetGroup.vue'
67
67
 
68
68
  // Organisms
69
- export { default as Asd20AccessibilityIssueModal } from './src/components/organisms/Asd20AccessibilityIssueModal/index.vue'
70
69
  export { default as Asd20ActionMenu } from './src/components/organisms/Asd20ActionMenu/index.vue'
71
70
  export { default as Asd20AiSearch } from './src/components/organisms/Asd20AiSearch/index.vue'
72
71
  export { default as Asd20AppHeader } from './src/components/organisms/Asd20AppHeader/index.vue'
package/helpers.js CHANGED
@@ -50,6 +50,5 @@ export { default as queryFiles } from './src/helpers/queryFiles.js'
50
50
  export { default as queryMessages } from './src/helpers/queryMessages.js'
51
51
  export { default as queryPages } from './src/helpers/queryPages.js'
52
52
  export { default as queryPeople } from './src/helpers/queryPeople.js'
53
- export { default as sendAccessibilityIssue } from './src/helpers/sendAccessibilityIssue.js'
54
53
  export { default as sendEmail } from './src/helpers/sendEmail.js'
55
54
  export { default as sortOrgsByLevel } from './src/helpers/sortOrgsByLevel.js'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@asd20/ui-next",
3
- "version": "2.24.3",
3
+ "version": "2.25.0",
4
4
  "private": false,
5
5
  "description": "ASD20 UI component library for Vue 3.",
6
6
  "license": "MIT",
@@ -25,6 +25,7 @@
25
25
  :required="field.required"
26
26
  :minlength="field.minLength"
27
27
  :maxlength="field.maxLength"
28
+ :max="dateMaximum"
28
29
  :show-character-count="showCharacterCount"
29
30
  :show-pristine-validation-errors="showPristineValidationErrors"
30
31
  :autocomplete="field.autocomplete || undefined"
@@ -123,6 +124,19 @@ import Asd20Checkbox from '../../atoms/Asd20Checkbox'
123
124
  import Asd20TextAreaInput from '../Asd20TextAreaInput'
124
125
  import Asd20TextInput from '../Asd20TextInput'
125
126
 
127
+ function districtToday() {
128
+ const parts = new Intl.DateTimeFormat('en-US', {
129
+ timeZone: 'America/Denver',
130
+ year: 'numeric',
131
+ month: '2-digit',
132
+ day: '2-digit',
133
+ }).formatToParts(new Date())
134
+ const values = Object.fromEntries(
135
+ parts.map(({ type, value }) => [type, value])
136
+ )
137
+ return `${values.year}-${values.month}-${values.day}`
138
+ }
139
+
126
140
  export default {
127
141
  name: 'Asd20PublicFormField',
128
142
  components: { Asd20Checkbox, Asd20TextAreaInput, Asd20TextInput },
@@ -141,6 +155,11 @@ export default {
141
155
  }
142
156
  },
143
157
  computed: {
158
+ dateMaximum() {
159
+ return this.field.type === 'date' && this.field.maxDate === 'today'
160
+ ? districtToday()
161
+ : undefined
162
+ },
144
163
  displayLabel() {
145
164
  return this.field.required
146
165
  ? `${this.field.label} (required)`
@@ -102,7 +102,10 @@
102
102
  class="asd20-public-form-modal__fallback"
103
103
  >
104
104
  <p>
105
- If you cannot submit this form, use one of these contact options:
105
+ {{
106
+ formDefinition.fallbackContact.instructions ||
107
+ 'If you cannot submit this form, use one of these contact options:'
108
+ }}
106
109
  </p>
107
110
  <a
108
111
  v-if="formDefinition.fallbackContact.email"
@@ -74,29 +74,3 @@ export function bindInjectedAccordionInteractions(root) {
74
74
  root.removeEventListener('click', onClick)
75
75
  }
76
76
  }
77
-
78
- export function bindAccessibilityModalTrigger(root, onOpen) {
79
- if (!root?.addEventListener) {
80
- return () => {}
81
- }
82
-
83
- const onClick = event => {
84
- const trigger = findClosestInRoot(
85
- event,
86
- '[data-trigger-accessibility-modal]',
87
- root
88
- )
89
-
90
- if (!trigger) {
91
- return
92
- }
93
-
94
- onOpen()
95
- }
96
-
97
- root.addEventListener('click', onClick)
98
-
99
- return () => {
100
- root.removeEventListener('click', onClick)
101
- }
102
- }
@@ -34,6 +34,12 @@ function optionalLength(value, label) {
34
34
  return value
35
35
  }
36
36
 
37
+ function optionalMaxDate(value, type) {
38
+ if (value == null) return undefined
39
+ if (type === 'date' && value === 'today') return 'today'
40
+ throw new Error('Maximum date is invalid.')
41
+ }
42
+
37
43
  function normalizeOptions(field) {
38
44
  if (!['select', 'radio'].includes(field.type)) return undefined
39
45
  if (
@@ -81,6 +87,7 @@ function normalizeField(field) {
81
87
  required: field.required === true,
82
88
  minLength: optionalLength(field.minLength, 'Minimum length'),
83
89
  maxLength: optionalLength(field.maxLength, 'Maximum length'),
90
+ maxDate: optionalMaxDate(field.maxDate, type),
84
91
  autocomplete: boundedText(field.autocomplete, 'Autocomplete value', 100),
85
92
  options: undefined,
86
93
  }
@@ -134,9 +141,14 @@ export function normalizePublicFormDefinition(definition) {
134
141
  }
135
142
  const email = boundedText(fallbackContact.email, 'Fallback email', 320)
136
143
  const phone = boundedText(fallbackContact.phone, 'Fallback phone', 50)
144
+ const instructions = boundedText(
145
+ fallbackContact.instructions,
146
+ 'Fallback instructions',
147
+ 1000
148
+ )
137
149
  if (email && !EMAIL_PATTERN.test(email))
138
150
  throw new Error('The public form fallback email is invalid.')
139
- normalizedFallback = { email, phone }
151
+ normalizedFallback = { email, phone, instructions }
140
152
  }
141
153
 
142
154
  const id = boundedText(definition.id, 'Form ID', 100, true)
@@ -183,6 +195,19 @@ function validDate(value) {
183
195
  )
184
196
  }
185
197
 
198
+ function districtToday() {
199
+ const parts = new Intl.DateTimeFormat('en-US', {
200
+ timeZone: 'America/Denver',
201
+ year: 'numeric',
202
+ month: '2-digit',
203
+ day: '2-digit',
204
+ }).formatToParts(new Date())
205
+ const values = Object.fromEntries(
206
+ parts.map(({ type, value }) => [type, value])
207
+ )
208
+ return `${values.year}-${values.month}-${values.day}`
209
+ }
210
+
186
211
  export function validatePublicFormValues(definition, sourceValues = {}) {
187
212
  const errors = {}
188
213
  const values = {}
@@ -220,6 +245,14 @@ export function validatePublicFormValues(definition, sourceValues = {}) {
220
245
  if (value && field.type === 'date' && !validDate(value)) {
221
246
  fieldErrors.push(`Enter a valid ${field.label.toLowerCase()}.`)
222
247
  }
248
+ if (
249
+ value &&
250
+ field.type === 'date' &&
251
+ field.maxDate === 'today' &&
252
+ value > districtToday()
253
+ ) {
254
+ fieldErrors.push(`${field.label} cannot be in the future.`)
255
+ }
223
256
  if (value && ['select', 'radio'].includes(field.type)) {
224
257
  const allowedValues = new Set(
225
258
  (field.options || []).map(option => option.value)
package/templates.js CHANGED
@@ -23,7 +23,6 @@ export { default as Asd20SalaryCalculatorTemplate } from './src/components/templ
23
23
  export { default as Asd20SchoolHomeTemplate } from './src/components/templates/Asd20SchoolHomeTemplate/index.vue'
24
24
  export { default as Asd20SchoolHomeVideoTemplate } from './src/components/templates/Asd20SchoolHomeVideoTemplate/index.vue'
25
25
  export { default as Asd20SearchAppTemplate } from './src/components/templates/Asd20SearchAppTemplate/index.vue'
26
- export { default as Asd20WayfindingAccessibilityTemplate } from './src/components/templates/Asd20WayfindingAccessibilityTemplate/index.vue'
27
26
  export { default as Asd20WayfindingAlternateTemplate } from './src/components/templates/Asd20WayfindingAlternateTemplate/index.vue'
28
27
  export { default as Asd20WayfindingImageTemplate } from './src/components/templates/Asd20WayfindingImageTemplate/index.vue'
29
28
  export { default as Asd20WayfindingTemplate } from './src/components/templates/Asd20WayfindingTemplate/index.vue'
@@ -1,375 +0,0 @@
1
- <template>
2
- <asd20-modal
3
- :open="open"
4
- title="Report an Accessibility Issue"
5
- icon="accessibility"
6
- dismissable
7
- class="asd20-accessibility-issue-modal"
8
- @dismiss="$emit('dismiss')"
9
- >
10
- <Asd20Viewport scrollable>
11
- <div
12
- v-if="submissionSuccess"
13
- class="submission-success"
14
- role="status"
15
- aria-live="polite"
16
- >
17
- <p style="text-align: center; font-weight: bold; font-size: 1.1rem;">
18
- Thank you! Your issue has been submitted.
19
- </p>
20
- </div>
21
-
22
- <template v-else>
23
- <asd20-text-input
24
- id="firstName"
25
- v-model="form.firstName"
26
- label="First Name"
27
- required
28
- @validated="validationErrors.firstName = $event"
29
- />
30
- <asd20-text-input
31
- id="lastName"
32
- v-model="form.lastName"
33
- label="Last Name"
34
- required
35
- @validated="validationErrors.lastName = $event"
36
- />
37
- <asd20-text-input
38
- id="email"
39
- v-model="form.email"
40
- label="Email Address"
41
- type="email"
42
- required
43
- :validator="validateEmailAddress"
44
- @validated="validationErrors.email = $event"
45
- />
46
- <asd20-text-input
47
- id="phone"
48
- v-model="form.phone"
49
- label="Phone Number"
50
- />
51
- <asd20-text-input
52
- id="address"
53
- v-model="form.address"
54
- label="Mailing Address"
55
- />
56
- <asd20-text-input
57
- id="organization"
58
- v-model="form.organization"
59
- label="Organization"
60
- />
61
- <asd20-text-area-input
62
- id="issue"
63
- v-model="form.issue"
64
- label="Describe the Issue"
65
- maxlength="2000"
66
- help-text="<ul style='margin: 0 0 0.5rem 1rem; font-size: 0.75rem !important;'>
67
- <li>Specify the exact URL, program, service, or digital asset you had a problem with.</li>
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
- <li>Describe what you believe should be done to resolve the issue.</li><br>
70
- </ul>"
71
- required
72
- :validator="validateIssue"
73
- @validated="validationErrors.issue = $event"
74
- />
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>
124
- </template>
125
- </Asd20Viewport>
126
- </asd20-modal>
127
- </template>
128
-
129
- <script>
130
- import Asd20Modal from '../../molecules/Asd20Modal'
131
- import Asd20TextInput from '../../molecules/Asd20TextInput'
132
- import Asd20TextAreaInput from '../../molecules/Asd20TextAreaInput'
133
- import Asd20Viewport from '../../atoms/Asd20Viewport'
134
- import Asd20Spinner from '../../atoms/Asd20Spinner'
135
- import Asd20Button from '../../atoms/Asd20Button'
136
- import Recaptcha from '../../utils/Recaptcha'
137
- import sendAccessibilityIssue, {
138
- resolveAccessibilityIssueEndpoint,
139
- } from '../../../helpers/sendAccessibilityIssue.js'
140
-
141
- export default {
142
- name: 'Asd20AccessibilityIssueModal',
143
- components: {
144
- Asd20Modal,
145
- Asd20TextInput,
146
- Asd20TextAreaInput,
147
- Asd20Viewport,
148
- Asd20Spinner,
149
- Asd20Button,
150
- Recaptcha,
151
- },
152
- props: {
153
- open: { type: Boolean, default: false },
154
- },
155
- data() {
156
- return {
157
- sending: false,
158
- submissionSuccess: false,
159
- feedbackMessage: '',
160
- feedbackRole: 'alert',
161
- form: {
162
- firstName: '',
163
- lastName: '',
164
- email: '',
165
- phone: '',
166
- address: '',
167
- organization: '',
168
- issue: '',
169
- website: '',
170
- },
171
- validationErrors: {
172
- firstName: [],
173
- lastName: [],
174
- email: [],
175
- issue: [],
176
- },
177
- }
178
- },
179
- computed: {
180
- isValid() {
181
- return Object.values(this.validationErrors).every(e => e.length === 0)
182
- },
183
- },
184
- watch: {
185
- open(value) {
186
- if (value) {
187
- this.submissionSuccess = false
188
- this.feedbackMessage = ''
189
- }
190
- },
191
- },
192
- methods: {
193
- validateEmailAddress({ value, validationErrors }) {
194
- const pattern = /^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/
195
- if (!pattern.test(value)) {
196
- validationErrors.push('A valid email address is required')
197
- }
198
- return validationErrors
199
- },
200
- validateIssue({ value, validationErrors }) {
201
- const maxLength = 2000
202
- if (!value || value.trim().length === 0) {
203
- validationErrors.push('Issue description is required.')
204
- } else if (value.length > maxLength) {
205
- validationErrors.push(
206
- `Issue description must be fewer than ${maxLength} characters.`
207
- )
208
- }
209
- return validationErrors
210
- },
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
-
293
- this.sending = true
294
- this.feedbackMessage = ''
295
-
296
- try {
297
- await submissionClient({
298
- ...this.form,
299
- captchaToken,
300
- })
301
- this.submissionSuccess = true
302
-
303
- // Wait 2 seconds, then close the modal
304
- setTimeout(() => {
305
- this.submissionSuccess = false
306
- this.form = {
307
- firstName: '',
308
- lastName: '',
309
- email: '',
310
- phone: '',
311
- address: '',
312
- organization: '',
313
- issue: '',
314
- website: '',
315
- }
316
- this.$emit('dismiss')
317
- }, 2000)
318
- } catch (e) {
319
- console.error('Accessibility issue submission failed:', e.message || e)
320
- this.showFeedback(this.getServerFailureMessage(e))
321
- } finally {
322
- this.sending = false
323
- }
324
- },
325
- },
326
- }
327
- </script>
328
-
329
- <style lang="scss" scoped>
330
- @use '../../../design/component-stack' as *;
331
-
332
- .asd20-accessibility-issue-modal {
333
- & :deep(.asd20-modal__content .asd20-viewport) {
334
- padding: space(1);
335
- }
336
- & :deep(.asd20-text-area-input textarea) {
337
- font-family: Arial, sans-serif;
338
- font-size: 1rem;
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
- }
361
- }
362
-
363
- @media (min-width: 1024px) {
364
- .asd20-accessibility-issue-modal {
365
- & :deep(.asd20-modal__content .asd20-viewport) {
366
- padding: space(1);
367
- }
368
- & :deep(.asd20-modal) {
369
- margin-top: auto;
370
- margin-bottom: auto;
371
- max-width: 40vw;
372
- }
373
- }
374
- }
375
- </style>
@@ -1,310 +0,0 @@
1
- <template>
2
- <div class="asd20-template-wayfinding-accessibility">
3
- <!-- Skip Nav -->
4
- <asd20-skip-to></asd20-skip-to>
5
-
6
- <!-- Site Navigation -->
7
- <asd20-site-navigation
8
- v-bind="siteNavigationSearchProps"
9
- :navigation="navigation"
10
- :action-items="actionItems"
11
- :menu-open="menuOpen"
12
- :search-open="searchOpen"
13
- :organization="organization"
14
- :organization-options="organizationOptions"
15
- @update:menuOpen="$emit('update:menuOpen', $event)"
16
- @update:searchOpen="$emit('update:searchOpen', $event)"
17
- >
18
- </asd20-site-navigation>
19
-
20
- <!-- Banner Notifications -->
21
- <asd20-notification-group
22
- :notifications="activeNotificationsByType.banner"
23
- group-type="banner"
24
- @dismiss="$emit('dismiss-notification', $event)"
25
- @toggle-all="$emit('toggle-all')"
26
- ></asd20-notification-group>
27
-
28
- <!-- Page Header -->
29
- <asd20-page-header
30
- v-show="pageHeaderContent"
31
- v-bind="pageHeaderContent"
32
- id="page-header"
33
- :organization="organization"
34
- :organization-options="organizationOptions"
35
- :language-code="languageCode"
36
- :breadcrumb-links="breadcrumbLinks"
37
- @update:languageCode="$emit('update:languageCode', $event)"
38
- >
39
- <!-- Floating Notifications -->
40
- <template #top>
41
- <asd20-notification-group
42
- class="asd20-notification-group--floating"
43
- :notifications="activeNotificationsByType.floating"
44
- :total-dismissed-notifications="totalDismissedNotifications"
45
- group-type="floating"
46
- @dismiss="$emit('dismiss-notification', $event)"
47
- @toggle-all="$emit('toggle-all')"
48
- ></asd20-notification-group>
49
- </template>
50
- <!-- <template #top><asd20-breadcrumb :links="breadcrumbLinks"></asd20-breadcrumb></template> -->
51
- </asd20-page-header>
52
-
53
- <!-- Inline-Notifications -->
54
- <asd20-notification-group
55
- class="asd20-notification-group--inline"
56
- :notifications="activeNotificationsByType.inline"
57
- group-type="inline"
58
- @dismiss="$emit('dismiss-notification', $event)"
59
- @toggle-all="$emit('toggle-all')"
60
- ></asd20-notification-group>
61
-
62
- <!-- Page Content -->
63
- <asd20-page-content
64
- class="intro-message"
65
- :primary-messages="primaryMessage"
66
- omit-detail-links
67
- omit-calls-to-action
68
- />
69
-
70
- <!-- Feed Section -->
71
- <asd20-feeds-section
72
- :announcements="announcements"
73
- :announcements-feed-props="announcementsFeedProps"
74
- :stories="stories"
75
- :stories-feed-props="storiesFeedProps"
76
- :events="events"
77
- :events-feed-props="eventsFeedProps"
78
- :organization="organization"
79
- :organization-options="organizationOptions"
80
- @announcements-in-view="$emit('announcements-in-view')"
81
- @stories-in-view="$emit('stories-in-view')"
82
- @events-in-view="$emit('events-in-view')"
83
- ></asd20-feeds-section>
84
-
85
- <asd20-page-content
86
- :class="wayFindingPrimaryMessages.length > 3 ? 'limit-message-width' : ''"
87
- :primary-messages="wayFindingPrimaryMessages"
88
- :secondary-messages="secondaryMessages"
89
- omit-body-content
90
- omit-calls-to-action
91
- grid
92
- />
93
-
94
- <!-- Photos & Videos -->
95
- <asd20-media-section
96
- v-if="firstMessage.images.length > 1 || firstMessage.videos.length"
97
- :images="firstMessage.images || []"
98
- :videos="firstMessage.videos || []"
99
- >
100
- </asd20-media-section>
101
-
102
- <!-- Sidebar Widgets -->
103
- <asd20-widgets-section
104
- v-if="widgetsSectionProps.sidebar"
105
- v-bind="widgetsSectionProps.sidebar"
106
- @events-in-view="$emit('events-in-view')"
107
- @files-in-view="$emit('files-in-view')"
108
- @people-in-view="$emit('people-in-view')"
109
- />
110
-
111
- <!-- Bottom Widgets -->
112
- <asd20-widgets-section
113
- v-if="widgetsSectionProps.footer || $slots.widgets"
114
- v-bind="widgetsSectionProps.footer"
115
- full
116
- @events-in-view="$emit('events-in-view')"
117
- @files-in-view="$emit('files-in-view')"
118
- @people-in-view="$emit('people-in-view')"
119
- >
120
- <slot
121
- name="widgets"
122
- />
123
- </asd20-widgets-section>
124
-
125
- <slot name="before-footer" />
126
-
127
- <!-- Quick Links -->
128
- <asd20-quicklinks-menu
129
- :quick-links="quickLinks"
130
- >
131
- <slot name="quicklinks-menu" />
132
- </asd20-quicklinks-menu>
133
-
134
- <!-- Footer -->
135
- <asd20-page-footer
136
- :organization="organization"
137
- :social-links="socialLinks"
138
- :disclosure-links="disclosureLinks"
139
- :website-logo-props="websiteLogoProps"
140
- :website-logo-props2="websiteLogoProps2"
141
- >
142
- <slot name="page-footer" />
143
- </asd20-page-footer>
144
- <Asd20AccessibilityIssueModal
145
- v-show="modalVisible"
146
- :open="modalOpen"
147
- @dismiss="modalOpen = false"
148
- />
149
- </div>
150
- </template>
151
-
152
- <script>
153
- // Components
154
- import Asd20SkipTo from '../../../components/atoms/Asd20SkipTo'
155
- import Asd20PageHeader from '../../../components/organisms/Asd20PageHeader'
156
-
157
- import Asd20SiteNavigation from '../../../components/organisms/Asd20SiteNavigation'
158
- import Asd20PageContent from '../../../components/organisms/Asd20PageContent'
159
-
160
- import Asd20WidgetsSection from '../../../components/organisms/Asd20WidgetsSection'
161
- import Asd20FeedsSection from '../../../components/organisms/Asd20FeedsSection'
162
- import Asd20MediaSection from '../../../components/organisms/Asd20MediaSection'
163
- import Asd20AccessibilityIssueModal from '../../../components/organisms/Asd20AccessibilityIssueModal'
164
- import Asd20PageFooter from '../../../components/organisms/Asd20PageFooter'
165
- import Asd20QuicklinksMenu from '../../../components/organisms/Asd20QuicklinksMenu'
166
- import Asd20NotificationGroup from '../../../components/organisms/Asd20NotificationGroup'
167
-
168
-
169
- // Mixins
170
- import pageTemplateMixin from '../../../mixins/pageTemplateMixin'
171
- import { bindAccessibilityModalTrigger } from '../../../helpers/injectedContentInteractions'
172
-
173
- export default {
174
- name: 'Asd20WayfindingAccessibilityTemplate',
175
- components: {
176
- Asd20PageContent,
177
- Asd20SkipTo,
178
- Asd20PageHeader,
179
- Asd20PageFooter,
180
- Asd20SiteNavigation,
181
- Asd20WidgetsSection,
182
- Asd20FeedsSection,
183
- Asd20NotificationGroup,
184
- Asd20QuicklinksMenu,
185
- Asd20MediaSection,
186
- Asd20AccessibilityIssueModal,
187
- },
188
- mixins: [pageTemplateMixin],
189
- data() {
190
- return {
191
- modalVisible: false,
192
- modalOpen: false,
193
- }
194
- },
195
- computed: {
196
- wayFindingPrimaryMessages() {
197
- // wayfinding page
198
- return (
199
- this.messages
200
- // Remove the first message, limit
201
- .slice(
202
- 1,
203
- Math.min(this.messages.length, this.primaryMessageLimit + 1)
204
- )
205
- )
206
- },
207
- primaryMessage() {
208
- const introMessage = this.primaryMessages.slice(0, 1)
209
- if (introMessage[0].bodyHtml === '' || null) return
210
- return introMessage
211
- },
212
- },
213
- mounted() {
214
- this.removeAccessibilityModalTrigger = bindAccessibilityModalTrigger(
215
- this.$el,
216
- () => {
217
- this.modalVisible = true
218
- this.$nextTick(() => {
219
- this.modalOpen = true
220
- })
221
- }
222
- )
223
- },
224
- beforeUnmount() {
225
- this.removeAccessibilityModalTrigger?.()
226
- },
227
- }
228
- </script>
229
-
230
- <style src="../../../design/tokens.css"></style>
231
-
232
- <style lang="scss">
233
- @use '../../../design/template-stack' as *;
234
- .asd20-template-wayfinding-accessibility {
235
- @include typography;
236
- @include template;
237
- display: flex;
238
- flex-direction: column;
239
- flex-grow: 1;
240
- flex-shrink: 0;
241
- overflow: hidden;
242
- margin-top: space(2.25);
243
- .asd20-notification-group--floating {
244
- position: absolute;
245
- top: space(2);
246
- right: space(0.5);
247
- }
248
- .asd20-notification-group--inline {
249
- margin-top: space(2) !important;
250
- }
251
-
252
- .intro-message {
253
- .primary-messaging-section {
254
- justify-content: center;
255
- margin-top: space(1) !important;
256
- padding: space(1) space(2) !important;
257
- .asd20-messaging {
258
- border-top: space(0.25) solid var(--color__tertiary);
259
- border-bottom: space(0.25) solid var(--color__tertiary);
260
- padding: space(1) 0;
261
- }
262
- }
263
-
264
- p,
265
- p + ul,
266
- p + ol {
267
- font-size: 1rem !important;
268
- line-height: 1.5;
269
- font-family: var(--website-typography__font-family-headlines);
270
- }
271
- h2 {
272
- @include asd20-font(
273
- 1.5,
274
- var(--website-typography__font-family-headlines),
275
- 1.2,
276
- 700
277
- );
278
- font-family: var(--website-typography__font-family-headlines);
279
- }
280
- }
281
- }
282
-
283
- @media (min-width: 1024px) {
284
- .asd20-template-wayfinding-accessibility {
285
- .asd20-notification-group--floating {
286
- position: absolute;
287
- top: 1rem;
288
- }
289
- @include template-desktop;
290
- margin-left: space(3);
291
- margin-top: 0;
292
- .asd20-page-content {
293
- margin-top: space(0);
294
- }
295
- }
296
- }
297
-
298
- @media (min-width: 1790px) {
299
- .limit-message-width {
300
- .primary-messaging-section,
301
- .secondary-messaging-section {
302
- .asd20-messaging {
303
- flex: 1 1 var(--minimum-column-width);
304
- margin: var(--gutter) !important;
305
- max-width: 30% !important;
306
- }
307
- }
308
- }
309
- }
310
- </style>
@@ -1,64 +0,0 @@
1
- import axios from 'axios'
2
-
3
- import { normalizeBaseUrl, resolveRuntimeConfigValue } from './runtimeConfig'
4
-
5
- const ACCESSIBILITY_ISSUE_ROUTE = '/api/accessibility/issues'
6
-
7
- function issueIsValid(issue) {
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
- )
15
- }
16
-
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()
28
-
29
- const functionsEndpoint = resolveRuntimeConfigValue({
30
- runtimeConfig,
31
- runtimeConfigKeys: ['functionsEndpoint'],
32
- envKeys: ['FUNCTIONS_ENDPOINT'],
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.')
42
- }
43
-
44
- if (!endpoint) {
45
- throw new Error('The accessibility issue endpoint is not configured.')
46
- }
47
-
48
- try {
49
- const response = await axios.post(endpoint, issue)
50
- return response.data
51
- } catch (error) {
52
- if (error.response) {
53
- const submissionError = new Error(
54
- `Accessibility submission error ${error.response.status}: ${JSON.stringify(
55
- error.response.data
56
- )}`
57
- )
58
- submissionError.response = error.response
59
- throw submissionError
60
- }
61
-
62
- throw new Error(`Accessibility submission request error: ${error.message}`)
63
- }
64
- }