@asd20/ui-next 2.30.0 → 2.30.1
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.30.1](https://github.com/academydistrict20/asd20-ui-next/compare/ui-next-v2.30.0...ui-next-v2.30.1) (2026-10-06)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* update styling on student records form ([3c9e73b](https://github.com/academydistrict20/asd20-ui-next/commit/3c9e73b669569aead47346fe52c5df432d1ecbf7))
|
|
9
|
+
|
|
3
10
|
# [2.30.0](https://github.com/academydistrict20/asd20-ui-next/compare/ui-next-v2.29.1...ui-next-v2.30.0) (2026-10-06)
|
|
4
11
|
|
|
5
12
|
|
package/package.json
CHANGED
|
@@ -5,26 +5,52 @@
|
|
|
5
5
|
class="asd20-public-form-field__upload"
|
|
6
6
|
>
|
|
7
7
|
<label :for="inputId">{{ displayLabel }}</label>
|
|
8
|
-
<
|
|
8
|
+
<asd20-button
|
|
9
9
|
:id="inputId"
|
|
10
|
+
label="Choose files"
|
|
11
|
+
horizontal
|
|
12
|
+
bordered
|
|
13
|
+
:aria-label="`Choose files for ${field.label.toLowerCase()}`"
|
|
14
|
+
:aria-describedby="describedBy"
|
|
15
|
+
:aria-invalid="hasErrors ? 'true' : undefined"
|
|
16
|
+
@click="$refs.fileInput.click()"
|
|
17
|
+
/>
|
|
18
|
+
<input
|
|
19
|
+
:id="`${inputId}-input`"
|
|
20
|
+
ref="fileInput"
|
|
10
21
|
type="file"
|
|
11
22
|
accept=".pdf,application/pdf"
|
|
12
23
|
multiple
|
|
24
|
+
hidden
|
|
13
25
|
:required="field.required"
|
|
14
|
-
:aria-
|
|
15
|
-
|
|
16
|
-
@change="
|
|
17
|
-
$emit('update:modelValue', Array.from($event.target.files || []))
|
|
18
|
-
"
|
|
26
|
+
:aria-label="field.label"
|
|
27
|
+
@change="addFiles"
|
|
19
28
|
/>
|
|
20
|
-
<
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
29
|
+
<div
|
|
30
|
+
class="asd20-public-form-field__file-selection"
|
|
31
|
+
aria-live="polite"
|
|
32
|
+
>
|
|
33
|
+
<ul
|
|
34
|
+
v-if="selectedFiles.length"
|
|
35
|
+
class="asd20-public-form-field__file-list"
|
|
24
36
|
>
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
37
|
+
<li
|
|
38
|
+
v-for="(file, index) in selectedFiles"
|
|
39
|
+
:key="index"
|
|
40
|
+
>
|
|
41
|
+
<span>{{ file.name }}</span>
|
|
42
|
+
<asd20-button
|
|
43
|
+
label="Remove"
|
|
44
|
+
text-only
|
|
45
|
+
:aria-label="`Remove ${file.name}`"
|
|
46
|
+
@click="removeFile(index)"
|
|
47
|
+
/>
|
|
48
|
+
</li>
|
|
49
|
+
</ul>
|
|
50
|
+
<p v-else>
|
|
51
|
+
No file chosen
|
|
52
|
+
</p>
|
|
53
|
+
</div>
|
|
28
54
|
</div>
|
|
29
55
|
<asd20-text-area-input
|
|
30
56
|
v-else-if="field.type === 'textarea'"
|
|
@@ -65,30 +91,36 @@
|
|
|
65
91
|
class="asd20-public-form-field__select"
|
|
66
92
|
>
|
|
67
93
|
<label :for="inputId">{{ displayLabel }}</label>
|
|
68
|
-
<
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
<option value="">
|
|
77
|
-
Please select
|
|
78
|
-
</option>
|
|
79
|
-
<option
|
|
80
|
-
v-for="option in field.options"
|
|
81
|
-
:key="option.value"
|
|
82
|
-
:value="option.value"
|
|
94
|
+
<div class="asd20-public-form-field__select-control">
|
|
95
|
+
<select
|
|
96
|
+
:id="inputId"
|
|
97
|
+
:value="modelValue"
|
|
98
|
+
:required="field.required"
|
|
99
|
+
:aria-describedby="describedBy"
|
|
100
|
+
:aria-invalid="hasErrors ? 'true' : undefined"
|
|
101
|
+
@change="$emit('update:modelValue', $event.target.value)"
|
|
83
102
|
>
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
103
|
+
<option value="">
|
|
104
|
+
Please select
|
|
105
|
+
</option>
|
|
106
|
+
<option
|
|
107
|
+
v-for="option in field.options"
|
|
108
|
+
:key="option.value"
|
|
109
|
+
:value="option.value"
|
|
110
|
+
>
|
|
111
|
+
{{ option.label }}
|
|
112
|
+
</option>
|
|
113
|
+
</select>
|
|
114
|
+
</div>
|
|
87
115
|
</div>
|
|
88
116
|
|
|
89
117
|
<fieldset
|
|
90
118
|
v-else-if="field.type === 'radio'"
|
|
91
119
|
class="asd20-public-form-field__choices"
|
|
120
|
+
:class="{
|
|
121
|
+
'asd20-public-form-field__choices--described':
|
|
122
|
+
field.options.some(option => option.description),
|
|
123
|
+
}"
|
|
92
124
|
:aria-describedby="describedBy"
|
|
93
125
|
:aria-invalid="hasErrors ? 'true' : undefined"
|
|
94
126
|
>
|
|
@@ -108,7 +140,7 @@
|
|
|
108
140
|
@change="$emit('update:modelValue', option.value)"
|
|
109
141
|
/>
|
|
110
142
|
<span>
|
|
111
|
-
{{ option.label }}
|
|
143
|
+
<strong class="asd20-public-form-field__option-title">{{ option.label }}</strong>
|
|
112
144
|
<span
|
|
113
145
|
v-if="option.description"
|
|
114
146
|
class="asd20-public-form-field__option-description"
|
|
@@ -161,6 +193,7 @@
|
|
|
161
193
|
</template>
|
|
162
194
|
|
|
163
195
|
<script>
|
|
196
|
+
import Asd20Button from '../../atoms/Asd20Button'
|
|
164
197
|
import Asd20Checkbox from '../../atoms/Asd20Checkbox'
|
|
165
198
|
import Asd20TextAreaInput from '../Asd20TextAreaInput'
|
|
166
199
|
import Asd20TextInput from '../Asd20TextInput'
|
|
@@ -180,7 +213,7 @@ function districtToday() {
|
|
|
180
213
|
|
|
181
214
|
export default {
|
|
182
215
|
name: 'Asd20PublicFormField',
|
|
183
|
-
components: { Asd20Checkbox, Asd20TextAreaInput, Asd20TextInput },
|
|
216
|
+
components: { Asd20Button, Asd20Checkbox, Asd20TextAreaInput, Asd20TextInput },
|
|
184
217
|
props: {
|
|
185
218
|
field: { type: Object, required: true },
|
|
186
219
|
modelValue: { type: [String, Boolean, Array], default: '' },
|
|
@@ -196,6 +229,9 @@ export default {
|
|
|
196
229
|
}
|
|
197
230
|
},
|
|
198
231
|
computed: {
|
|
232
|
+
selectedFiles() {
|
|
233
|
+
return Array.isArray(this.modelValue) ? this.modelValue : []
|
|
234
|
+
},
|
|
199
235
|
dateMaximum() {
|
|
200
236
|
return this.field.type === 'date' && this.field.maxDate === 'today'
|
|
201
237
|
? districtToday()
|
|
@@ -239,6 +275,21 @@ export default {
|
|
|
239
275
|
)
|
|
240
276
|
},
|
|
241
277
|
},
|
|
278
|
+
methods: {
|
|
279
|
+
addFiles(event) {
|
|
280
|
+
const addedFiles = Array.from(event.target.files || [])
|
|
281
|
+
if (addedFiles.length) {
|
|
282
|
+
this.$emit('update:modelValue', [...this.selectedFiles, ...addedFiles])
|
|
283
|
+
}
|
|
284
|
+
// The model owns the selection; reset the picker so later choices append
|
|
285
|
+
// and a removed document can be selected again.
|
|
286
|
+
event.target.value = ''
|
|
287
|
+
},
|
|
288
|
+
removeFile(index) {
|
|
289
|
+
this.$emit('update:modelValue', this.selectedFiles.filter((_, fileIndex) => fileIndex !== index))
|
|
290
|
+
this.$nextTick(() => document.getElementById(this.inputId)?.focus())
|
|
291
|
+
},
|
|
292
|
+
},
|
|
242
293
|
}
|
|
243
294
|
</script>
|
|
244
295
|
|
|
@@ -246,6 +297,44 @@ export default {
|
|
|
246
297
|
@use '../../../design/component-stack' as *;
|
|
247
298
|
|
|
248
299
|
.asd20-public-form-field {
|
|
300
|
+
&__option-title {
|
|
301
|
+
font-weight: 700;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
&__upload {
|
|
305
|
+
align-items: flex-start;
|
|
306
|
+
display: flex;
|
|
307
|
+
flex-direction: column;
|
|
308
|
+
gap: 0.5rem;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
&__file-selection {
|
|
312
|
+
width: 100%;
|
|
313
|
+
|
|
314
|
+
p {
|
|
315
|
+
margin: 0;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
&__file-list {
|
|
320
|
+
display: flex;
|
|
321
|
+
flex-direction: column;
|
|
322
|
+
gap: 0.5rem;
|
|
323
|
+
list-style: none;
|
|
324
|
+
margin: 0;
|
|
325
|
+
padding: 0;
|
|
326
|
+
|
|
327
|
+
li {
|
|
328
|
+
align-items: baseline;
|
|
329
|
+
display: flex;
|
|
330
|
+
gap: 0.5rem;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
span {
|
|
334
|
+
overflow-wrap: anywhere;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
249
338
|
&__option-description {
|
|
250
339
|
display: block;
|
|
251
340
|
font-weight: normal;
|
|
@@ -272,7 +361,24 @@ export default {
|
|
|
272
361
|
margin-bottom: space(0.125);
|
|
273
362
|
}
|
|
274
363
|
|
|
364
|
+
&__select-control {
|
|
365
|
+
position: relative;
|
|
366
|
+
|
|
367
|
+
&::after {
|
|
368
|
+
border-left: 0.5rem solid transparent;
|
|
369
|
+
border-right: 0.5rem solid transparent;
|
|
370
|
+
border-top: 0.5rem solid currentColor;
|
|
371
|
+
content: '';
|
|
372
|
+
pointer-events: none;
|
|
373
|
+
position: absolute;
|
|
374
|
+
right: 1rem;
|
|
375
|
+
top: 50%;
|
|
376
|
+
transform: translateY(-50%);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
275
380
|
select {
|
|
381
|
+
appearance: none;
|
|
276
382
|
background: white;
|
|
277
383
|
border: 2px solid var(--color__accent);
|
|
278
384
|
border-radius: var(--website-shape__radius-s);
|
|
@@ -280,6 +386,7 @@ export default {
|
|
|
280
386
|
color: var(--website-card__reverse-background-color);
|
|
281
387
|
font: inherit;
|
|
282
388
|
padding: space(0.25);
|
|
389
|
+
padding-right: 3rem;
|
|
283
390
|
width: 100%;
|
|
284
391
|
}
|
|
285
392
|
|
|
@@ -296,6 +403,15 @@ export default {
|
|
|
296
403
|
margin: space(0.125) 0;
|
|
297
404
|
}
|
|
298
405
|
|
|
406
|
+
&__choices--described label {
|
|
407
|
+
align-items: flex-start;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
&__choices input[type='radio'] {
|
|
411
|
+
flex: 0 0 auto;
|
|
412
|
+
margin: 0;
|
|
413
|
+
}
|
|
414
|
+
|
|
299
415
|
&__help,
|
|
300
416
|
&__character-count,
|
|
301
417
|
&__errors {
|
|
@@ -143,6 +143,7 @@
|
|
|
143
143
|
>
|
|
144
144
|
<asd20-button
|
|
145
145
|
:label="formDefinition.submitLabel"
|
|
146
|
+
:disabled="!canSubmit"
|
|
146
147
|
horizontal
|
|
147
148
|
centered
|
|
148
149
|
bordered
|
|
@@ -152,6 +153,7 @@
|
|
|
152
153
|
<asd20-button
|
|
153
154
|
v-else
|
|
154
155
|
:label="formDefinition.submitLabel"
|
|
156
|
+
:disabled="!canSubmit"
|
|
155
157
|
horizontal
|
|
156
158
|
centered
|
|
157
159
|
bordered
|
|
@@ -266,6 +268,13 @@ export default {
|
|
|
266
268
|
}
|
|
267
269
|
},
|
|
268
270
|
computed: {
|
|
271
|
+
canSubmit() {
|
|
272
|
+
return Boolean(
|
|
273
|
+
this.formDefinition &&
|
|
274
|
+
!this.sending &&
|
|
275
|
+
validatePublicFormValues(this.formDefinition, this.values).valid
|
|
276
|
+
)
|
|
277
|
+
},
|
|
269
278
|
visibleFields() {
|
|
270
279
|
return (this.formDefinition?.fields || [])
|
|
271
280
|
.filter(field => isPublicFormFieldVisible(field, this.values))
|
|
@@ -107,6 +107,7 @@ function normalizeField(field) {
|
|
|
107
107
|
),
|
|
108
108
|
options: undefined,
|
|
109
109
|
visibleWhen: normalizeCondition(field.visibleWhen),
|
|
110
|
+
requiredUnlessUploaded: boundedText(field.requiredUnlessUploaded, 'Upload field name', 64),
|
|
110
111
|
requiredWhen:
|
|
111
112
|
field.requiredWhen == null
|
|
112
113
|
? undefined
|
|
@@ -191,13 +192,13 @@ export function isPublicFormFieldVisible(field, values) {
|
|
|
191
192
|
}
|
|
192
193
|
|
|
193
194
|
export function isPublicFormFieldRequired(field, values) {
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
field.requiredWhen?.some(condition =>
|
|
197
|
-
condition.values.includes(String(values[condition.fieldName] || '').trim())
|
|
198
|
-
) ||
|
|
199
|
-
false
|
|
195
|
+
const required = field.required || field.requiredWhen?.some(condition =>
|
|
196
|
+
condition.values.includes(String(values[condition.fieldName] || '').trim())
|
|
200
197
|
)
|
|
198
|
+
const uploaded = field.requiredUnlessUploaded &&
|
|
199
|
+
Array.isArray(values[field.requiredUnlessUploaded]) &&
|
|
200
|
+
values[field.requiredUnlessUploaded].length > 0
|
|
201
|
+
return Boolean(required && !uploaded)
|
|
201
202
|
}
|
|
202
203
|
|
|
203
204
|
export async function serializePublicFormValues(definition, values) {
|
|
@@ -252,6 +253,12 @@ export function normalizePublicFormDefinition(definition) {
|
|
|
252
253
|
throw new Error('The public form contains duplicate field names.')
|
|
253
254
|
|
|
254
255
|
for (const field of fields) {
|
|
256
|
+
if (field.requiredUnlessUploaded) {
|
|
257
|
+
const upload = fields.find(candidate => candidate.name === field.requiredUnlessUploaded)
|
|
258
|
+
if (!upload || upload.type !== 'file' || upload === field) {
|
|
259
|
+
throw new Error('The public form has an invalid upload requirement reference.')
|
|
260
|
+
}
|
|
261
|
+
}
|
|
255
262
|
for (const condition of [
|
|
256
263
|
field.visibleWhen,
|
|
257
264
|
...(field.requiredWhen || []),
|