@asd20/ui-next 2.24.2 → 2.24.4

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,20 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.24.4](https://github.com/academydistrict20/asd20-ui-next/compare/ui-next-v2.24.3...ui-next-v2.24.4) (2026-09-17)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * remove legacy accessibility form templates and components ([3e9fa07](https://github.com/academydistrict20/asd20-ui-next/commit/3e9fa0749a9000f9c8970bbb8d1a81ed665a94e8))
9
+
10
+ ## [2.24.3](https://github.com/academydistrict20/asd20-ui-next/compare/ui-next-v2.24.2...ui-next-v2.24.3) (2026-09-17)
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * fix accessibility form verbiage; add native trigger support for public forms ([54f5b47](https://github.com/academydistrict20/asd20-ui-next/commit/54f5b47b2bb80fbf807439fcfd376e911806e053))
16
+ * fix release verifier from last commit ([b7b8d8f](https://github.com/academydistrict20/asd20-ui-next/commit/b7b8d8f996c757ddd727ab2272f38ea11737ddb7))
17
+
3
18
  ## [2.24.2](https://github.com/academydistrict20/asd20-ui-next/compare/ui-next-v2.24.1...ui-next-v2.24.2) (2026-09-17)
4
19
 
5
20
 
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.2",
3
+ "version": "2.24.4",
4
4
  "private": false,
5
5
  "description": "ASD20 UI component library for Vue 3.",
6
6
  "license": "MIT",
@@ -39,6 +39,7 @@ export default {
39
39
  hideLabel: { type: Boolean, default: false },
40
40
  link: { type: String, default: '' },
41
41
  trackingCode: { type: String, default: '' }, // e.g., 'data-analytics-id'
42
+ publicFormId: { type: String, default: '' },
42
43
  icon: { type: String, default: '' },
43
44
  badge: { type: Number, default: 0 },
44
45
  size: {
@@ -73,7 +74,13 @@ export default {
73
74
  }
74
75
  },
75
76
  tag() {
76
- return !this.link || this.link === ' ' ? 'button' : 'a'
77
+ return this.normalizedPublicFormId || !this.link || this.link === ' '
78
+ ? 'button'
79
+ : 'a'
80
+ },
81
+ normalizedPublicFormId() {
82
+ const formId = String(this.publicFormId || '').trim()
83
+ return /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(formId) ? formId : ''
77
84
  },
78
85
  isExternal() {
79
86
  return shouldOpenInNewWindow(this.link)
@@ -109,16 +116,21 @@ export default {
109
116
 
110
117
  // Optional tracking code (dynamic attribute key)
111
118
  if (this.trackingCode) a[this.trackingCode] = ''
119
+ if (this.normalizedPublicFormId) {
120
+ a['data-public-form'] = this.normalizedPublicFormId
121
+ }
112
122
 
113
123
  return a
114
124
  },
115
125
  },
116
126
  methods: {
117
127
  handleClick(event) {
118
- navigateInternalHref(this, event, this.link, {
119
- target: event.currentTarget?.getAttribute?.('target'),
120
- download: event.currentTarget?.hasAttribute?.('download'),
121
- })
128
+ if (!this.normalizedPublicFormId) {
129
+ navigateInternalHref(this, event, this.link, {
130
+ target: event.currentTarget?.getAttribute?.('target'),
131
+ download: event.currentTarget?.hasAttribute?.('download'),
132
+ })
133
+ }
122
134
  this.forwardEvent('click', event)
123
135
  },
124
136
  forwardEvent(name, event) {
@@ -8,6 +8,8 @@
8
8
  :required="field.required"
9
9
  :minlength="field.minLength"
10
10
  :maxlength="field.maxLength"
11
+ :show-character-count="showCharacterCount"
12
+ :show-pristine-validation-errors="showPristineValidationErrors"
11
13
  :autocomplete="field.autocomplete || undefined"
12
14
  :aria-describedby="describedBy"
13
15
  :aria-invalid="hasErrors ? 'true' : undefined"
@@ -23,6 +25,8 @@
23
25
  :required="field.required"
24
26
  :minlength="field.minLength"
25
27
  :maxlength="field.maxLength"
28
+ :show-character-count="showCharacterCount"
29
+ :show-pristine-validation-errors="showPristineValidationErrors"
26
30
  :autocomplete="field.autocomplete || undefined"
27
31
  :aria-describedby="describedBy"
28
32
  :aria-invalid="hasErrors ? 'true' : undefined"
@@ -127,6 +131,8 @@ export default {
127
131
  modelValue: { type: [String, Boolean], default: '' },
128
132
  errors: { type: Array, default: () => [] },
129
133
  inputId: { type: String, required: true },
134
+ showCharacterCount: { type: Boolean, default: true },
135
+ showPristineValidationErrors: { type: Boolean, default: true },
130
136
  },
131
137
  emits: ['update:modelValue'],
132
138
  data() {
@@ -25,7 +25,7 @@
25
25
  <slot name="suffix" />
26
26
  </div>
27
27
  <div
28
- v-if="validationErrors.length || maxLength !== null"
28
+ v-if="renderedValidationErrors.length || hasCharacterCount"
29
29
  class="asd20-text-area-input__validation-row"
30
30
  >
31
31
  <div class="asd20-text-area-input__validation-errors">
@@ -37,7 +37,7 @@
37
37
  />
38
38
  </div>
39
39
  <span
40
- v-if="maxLength !== null"
40
+ v-if="hasCharacterCount"
41
41
  class="asd20-text-area-input__character-count"
42
42
  >{{ characterCountText }}</span>
43
43
  </div>
@@ -21,7 +21,7 @@
21
21
  <slot name="suffix" />
22
22
  </div>
23
23
  <div
24
- v-if="validationErrors.length || maxLength !== null"
24
+ v-if="renderedValidationErrors.length || hasCharacterCount"
25
25
  class="asd20-text-input__validation-row"
26
26
  >
27
27
  <div class="asd20-text-input__validation-errors">
@@ -33,7 +33,7 @@
33
33
  />
34
34
  </div>
35
35
  <span
36
- v-if="maxLength !== null"
36
+ v-if="hasCharacterCount"
37
37
  class="asd20-text-input__character-count"
38
38
  >{{ characterCountText }}</span>
39
39
  </div>
@@ -67,6 +67,8 @@
67
67
  :label="cta.label"
68
68
  :icon="cta.icon"
69
69
  :link="cta.url"
70
+ :tracking-code="cta.trackingCode"
71
+ :public-form-id="cta.publicFormId"
70
72
  reversed
71
73
  horizontal
72
74
  centered
@@ -158,6 +158,8 @@
158
158
  :label="callsToAction[0].label"
159
159
  :icon="callsToAction[0].icon"
160
160
  :link="callsToAction[0].url"
161
+ :tracking-code="callsToAction[0].trackingCode"
162
+ :public-form-id="callsToAction[0].publicFormId"
161
163
  reversed
162
164
  horizontal
163
165
  centered
@@ -168,6 +170,8 @@
168
170
  :label="callsToAction[1].label"
169
171
  :icon="callsToAction[1].icon"
170
172
  :link="callsToAction[1].url"
173
+ :tracking-code="callsToAction[1].trackingCode"
174
+ :public-form-id="callsToAction[1].publicFormId"
171
175
  reversed
172
176
  horizontal
173
177
  centered
@@ -150,6 +150,7 @@
150
150
  :icon="callsToAction[0].icon"
151
151
  :link="callsToAction[0].url"
152
152
  :tracking-code="callsToAction[0].trackingCode"
153
+ :public-form-id="callsToAction[0].publicFormId"
153
154
  reversed
154
155
  horizontal
155
156
  centered
@@ -162,6 +163,7 @@
162
163
  :icon="callsToAction[1].icon"
163
164
  :link="callsToAction[1].url"
164
165
  :tracking-code="callsToAction[1].trackingCode"
166
+ :public-form-id="callsToAction[1].publicFormId"
165
167
  reversed
166
168
  horizontal
167
169
  centered
@@ -2,7 +2,7 @@
2
2
  <asd20-modal
3
3
  :open="open"
4
4
  :title="modalTitle"
5
- icon="contact"
5
+ icon="website"
6
6
  dismissable
7
7
  class="asd20-public-form-modal"
8
8
  @dismiss="dismiss"
@@ -77,6 +77,8 @@
77
77
  :input-id="fieldId(field.name)"
78
78
  :model-value="values[field.name]"
79
79
  :errors="validationErrors[field.name] || []"
80
+ :show-character-count="shouldShowCharacterCount(field)"
81
+ :show-pristine-validation-errors="showPristineValidationErrors"
80
82
  @update:modelValue="updateValue(field.name, $event)"
81
83
  />
82
84
 
@@ -230,6 +232,9 @@ export default {
230
232
  modalTitle() {
231
233
  return this.formDefinition?.title || 'Public Form'
232
234
  },
235
+ showPristineValidationErrors() {
236
+ return this.formDefinition?.id !== 'accessibility-issue'
237
+ },
233
238
  errorEntries() {
234
239
  if (!this.formDefinition) return []
235
240
  return this.formDefinition.fields
@@ -321,6 +326,9 @@ export default {
321
326
  fieldId(name) {
322
327
  return `${this.instanceId}-${name}`
323
328
  },
329
+ shouldShowCharacterCount(field) {
330
+ return this.formDefinition.id !== 'accessibility-issue' || field.name === 'issue'
331
+ },
324
332
  updateValue(name, value) {
325
333
  this.values[name] = value
326
334
  if (this.validationAttempted) this.validate(false)
@@ -352,7 +360,7 @@ export default {
352
360
  },
353
361
  captchaFailed() {
354
362
  this.showFeedback(
355
- 'The verification service could not start. Please try again or use the contact options below.'
363
+ 'The verification service could not start. Please try again or use the contact options listed.'
356
364
  )
357
365
  },
358
366
  captchaExpired() {
@@ -368,15 +376,15 @@ export default {
368
376
  if (status === 403)
369
377
  return 'The verification was not accepted. Please try again.'
370
378
  if (status === 429) {
371
- return 'Too many submissions were received recently. Please wait and try again, or use the contact options below.'
379
+ return 'Too many submissions were received recently. Please wait and try again, or use the contact options listed.'
372
380
  }
373
381
  if (status === 502) {
374
- return 'The form could not be delivered right now. Please try again later or use the contact options below.'
382
+ return 'The form could not be delivered right now. Please try again later or use the contact options listed.'
375
383
  }
376
384
  if (status === 503) {
377
- return 'This form is temporarily unavailable. Please use the contact options below.'
385
+ return 'This form is temporarily unavailable. Please use the contact options listed.'
378
386
  }
379
- return 'Something went wrong while submitting the form. Please try again later or use the contact options below.'
387
+ return 'Something went wrong while submitting the form. Please try again later or use the contact options listed.'
380
388
  },
381
389
  showFeedback(message, role = 'alert') {
382
390
  this.feedbackMessage = message
@@ -62,6 +62,8 @@
62
62
  :label="cta.label"
63
63
  :icon="cta.icon"
64
64
  :link="cta.url"
65
+ :tracking-code="cta.trackingCode"
66
+ :public-form-id="cta.publicFormId"
65
67
  reversed
66
68
  horizontal
67
69
  centered
@@ -88,6 +88,8 @@
88
88
  :label="cta.label"
89
89
  :icon="cta.icon"
90
90
  :link="cta.url"
91
+ :tracking-code="cta.trackingCode"
92
+ :public-form-id="cta.publicFormId"
91
93
  reversed
92
94
  horizontal
93
95
  centered
@@ -57,6 +57,8 @@
57
57
  :label="cta.label"
58
58
  :icon="cta.icon"
59
59
  :link="cta.url"
60
+ :tracking-code="cta.trackingCode"
61
+ :public-form-id="cta.publicFormId"
60
62
  bordered
61
63
  reversed
62
64
  horizontal
@@ -25,6 +25,7 @@ query($pageId: uuid!, $now: String) {
25
25
  id
26
26
  title
27
27
  trackingCode
28
+ publicFormId
28
29
  url
29
30
  description
30
31
  }
@@ -84,6 +85,7 @@ query($pageId: uuid!, $now: String) {
84
85
  id
85
86
  title
86
87
  trackingCode
88
+ publicFormId
87
89
  url
88
90
  }
89
91
  }
@@ -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
- }
@@ -25,6 +25,8 @@ export default {
25
25
  modelValue: { default: '' },
26
26
  label: { type: String, default: '' },
27
27
  hideLabel: { type: Boolean, default: false },
28
+ showCharacterCount: { type: Boolean, default: true },
29
+ showPristineValidationErrors: { type: Boolean, default: true },
28
30
  helpText: { type: String, default: '' },
29
31
  items: { type: Array, default: () => [] },
30
32
  itemDefault: { type: Object, default: null },
@@ -101,6 +103,8 @@ export default {
101
103
  return this.validationErrors.length === 0
102
104
  },
103
105
  renderedValidationErrors() {
106
+ if (!this.isDirty && !this.showPristineValidationErrors) return []
107
+
104
108
  return this.validationErrors.map(error =>
105
109
  !this.isDirty && error === REQUIRED_VALIDATION_ERROR
106
110
  ? 'Required'
@@ -127,13 +131,16 @@ export default {
127
131
  },
128
132
 
129
133
  computedHelpText() {
130
- if (this.maxLength !== null) {
134
+ if (this.hasCharacterCount) {
131
135
  return [this.helpText, this.characterCountText].filter(Boolean).join(' ')
132
136
  }
133
137
  return this.helpText
134
138
  },
139
+ hasCharacterCount() {
140
+ return this.showCharacterCount && this.maxLength !== null
141
+ },
135
142
  characterCountText() {
136
- if (this.maxLength === null) return ''
143
+ if (!this.hasCharacterCount) return ''
137
144
  return `${(this.resolvedValue || '').length}/${this.maxLength}`
138
145
  },
139
146
  maxLength() {
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
- }