mirror of
https://github.com/go-vikunja/vikunja.git
synced 2026-07-23 14:32:08 -05:00
Wire aria-invalid, aria-describedby and role=alert on the form primitive components so errors raised directly on FormInput or FormSelect are announced by assistive tech and programmatically linked to the control.
106 lines
2.2 KiB
Vue
106 lines
2.2 KiB
Vue
<script setup lang="ts">
|
|
import {computed, useId} from 'vue'
|
|
|
|
export type SelectOption =
|
|
| string
|
|
| number
|
|
| {value: string | number, label: string, disabled?: boolean}
|
|
|
|
interface Props {
|
|
modelValue?: string | number | null
|
|
modelModifiers?: {number?: boolean}
|
|
id?: string
|
|
disabled?: boolean
|
|
loading?: boolean
|
|
error?: string | null
|
|
options?: SelectOption[]
|
|
}
|
|
|
|
const props = withDefaults(defineProps<Props>(), {
|
|
modelModifiers: () => ({}),
|
|
})
|
|
const emit = defineEmits<{
|
|
'update:modelValue': [value: string | number]
|
|
}>()
|
|
|
|
defineOptions({inheritAttrs: false})
|
|
|
|
const fallbackId = useId()
|
|
const selectId = computed(() => props.id ?? fallbackId)
|
|
const errorId = computed(() => props.error ? `${selectId.value}-error` : undefined)
|
|
|
|
const wrapperClasses = computed(() => [
|
|
'select',
|
|
{'is-loading': props.loading},
|
|
])
|
|
|
|
const selectBindings = computed(() => {
|
|
const bindings: Record<string, unknown> = {}
|
|
if (props.modelValue !== undefined) {
|
|
bindings.value = props.modelValue
|
|
}
|
|
return bindings
|
|
})
|
|
|
|
const normalizedOptions = computed(() => {
|
|
if (!props.options) {
|
|
return null
|
|
}
|
|
return props.options.map(opt => {
|
|
if (typeof opt === 'object' && opt !== null) {
|
|
return opt
|
|
}
|
|
return {value: opt, label: String(opt)}
|
|
})
|
|
})
|
|
|
|
function handleChange(event: Event) {
|
|
const value = (event.target as HTMLSelectElement).value
|
|
const shouldCoerceNumber = props.modelModifiers.number || typeof props.modelValue === 'number'
|
|
if (shouldCoerceNumber) {
|
|
emit('update:modelValue', value === '' ? '' : Number(value))
|
|
} else {
|
|
emit('update:modelValue', value)
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div :class="wrapperClasses">
|
|
<select
|
|
:id="selectId"
|
|
v-bind="{ ...$attrs, ...selectBindings }"
|
|
:disabled="disabled || undefined"
|
|
:aria-invalid="error ? true : undefined"
|
|
:aria-describedby="errorId"
|
|
@change="handleChange"
|
|
>
|
|
<template v-if="normalizedOptions">
|
|
<option
|
|
v-for="opt in normalizedOptions"
|
|
:key="opt.value"
|
|
:value="opt.value"
|
|
:disabled="opt.disabled || undefined"
|
|
>
|
|
{{ opt.label }}
|
|
</option>
|
|
</template>
|
|
<slot v-else />
|
|
</select>
|
|
</div>
|
|
<p
|
|
v-if="error"
|
|
:id="errorId"
|
|
class="help is-danger"
|
|
role="alert"
|
|
>
|
|
{{ error }}
|
|
</p>
|
|
</template>
|
|
|
|
<style lang="scss" scoped>
|
|
.select select {
|
|
inline-size: 100%;
|
|
}
|
|
</style>
|