mirror of
https://github.com/ArthurDanjou/ui.git
synced 2026-02-02 05:08:03 +01:00
fix(FormGroup): use explicit label instead of implicit label (#638)
This commit is contained in:
@@ -1,20 +1,21 @@
|
|||||||
<template>
|
<template>
|
||||||
<div :class="wrapperClass" v-bind="attrs">
|
<div :class="wrapperClass" v-bind="attrs">
|
||||||
<label>
|
<div v-if="label" :class="[ui.label.wrapper, size]">
|
||||||
<div v-if="label" :class="[ui.label.wrapper, size]">
|
<label :for="labelFor" :class="[ui.label.base, required ? ui.label.required : '']">{{ label }}</label>
|
||||||
<p :class="[ui.label.base, required ? ui.label.required : '']">{{ label }}</p>
|
<span v-if="hint" :class="[ui.hint]">{{ hint }}</span>
|
||||||
<span v-if="hint" :class="[ui.hint]">{{ hint }}</span>
|
</div>
|
||||||
</div>
|
<p v-if="description" :class="[ui.description, size]">
|
||||||
|
{{ description }}
|
||||||
<p v-if="description" :class="[ui.description, size]">{{ description }}</p>
|
</p>
|
||||||
|
<div :class="[label ? ui.container : '']">
|
||||||
<div :class="[label ? ui.container : '']" @click="$event.preventDefault()">
|
<slot v-bind="{ error }" />
|
||||||
<slot v-bind="{ error }" />
|
<p v-if="typeof error === 'string' && error" :class="[ui.error, size]">
|
||||||
|
{{ error }}
|
||||||
<p v-if="error && typeof error !== 'boolean'" :class="[ui.error, size]">{{ error }}</p>
|
</p>
|
||||||
<p v-else-if="help" :class="[ui.help, size]">{{ help }}</p>
|
<p v-else-if="help" :class="[ui.help, size]">
|
||||||
</div>
|
{{ help }}
|
||||||
</label>
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -23,7 +24,7 @@ import { computed, defineComponent, provide, inject } from 'vue'
|
|||||||
import type { PropType } from 'vue'
|
import type { PropType } from 'vue'
|
||||||
import { omit } from '../../utils/lodash'
|
import { omit } from '../../utils/lodash'
|
||||||
import { twMerge } from 'tailwind-merge'
|
import { twMerge } from 'tailwind-merge'
|
||||||
import type { FormError } from '../../types/form'
|
import type { FormError, InjectedFormGroupValue } from '../../types/form'
|
||||||
import { defuTwMerge } from '../../utils'
|
import { defuTwMerge } from '../../utils'
|
||||||
import { useAppConfig } from '#imports'
|
import { useAppConfig } from '#imports'
|
||||||
// TODO: Remove
|
// TODO: Remove
|
||||||
@@ -32,6 +33,8 @@ import appConfig from '#build/app.config'
|
|||||||
|
|
||||||
// const appConfig = useAppConfig()
|
// const appConfig = useAppConfig()
|
||||||
|
|
||||||
|
let increment = 0
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
inheritAttrs: false,
|
inheritAttrs: false,
|
||||||
props: {
|
props: {
|
||||||
@@ -92,14 +95,17 @@ export default defineComponent({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const size = computed(() => ui.value.size[props.size ?? appConfig.ui.input.default.size])
|
const size = computed(() => ui.value.size[props.size ?? appConfig.ui.input.default.size])
|
||||||
|
const labelFor = ref(`${props.name || 'lf'}-${increment = increment < 1000000 ? increment + 1 : 0}`)
|
||||||
|
|
||||||
provide('form-group', {
|
provide<InjectedFormGroupValue>('form-group', {
|
||||||
error,
|
error,
|
||||||
|
labelFor,
|
||||||
name: computed(() => props.name),
|
name: computed(() => props.name),
|
||||||
size: computed(() => props.size)
|
size: computed(() => props.size)
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
labelFor,
|
||||||
attrs: computed(() => omit(attrs, ['class'])),
|
attrs: computed(() => omit(attrs, ['class'])),
|
||||||
// eslint-disable-next-line vue/no-dupe-keys
|
// eslint-disable-next-line vue/no-dupe-keys
|
||||||
ui,
|
ui,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div :class="wrapperClass">
|
<div :class="wrapperClass">
|
||||||
<input
|
<input
|
||||||
|
:id="labelFor"
|
||||||
ref="input"
|
ref="input"
|
||||||
:name="name"
|
:name="name"
|
||||||
:value="modelValue"
|
:value="modelValue"
|
||||||
@@ -59,6 +60,10 @@ export default defineComponent({
|
|||||||
type: String,
|
type: String,
|
||||||
default: 'text'
|
default: 'text'
|
||||||
},
|
},
|
||||||
|
id: {
|
||||||
|
type: String,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
name: {
|
name: {
|
||||||
type: String,
|
type: String,
|
||||||
default: null
|
default: null
|
||||||
@@ -151,9 +156,10 @@ export default defineComponent({
|
|||||||
|
|
||||||
const ui = computed<Partial<typeof appConfig.ui.input>>(() => defuTwMerge({}, props.ui, appConfig.ui.input))
|
const ui = computed<Partial<typeof appConfig.ui.input>>(() => defuTwMerge({}, props.ui, appConfig.ui.input))
|
||||||
|
|
||||||
const { emitFormBlur, emitFormInput, formGroup } = useFormGroup()
|
const { emitFormBlur, emitFormInput, formGroup } = useFormGroup(props)
|
||||||
const color = computed(() => formGroup?.error?.value ? 'red' : props.color)
|
const color = computed(() => formGroup?.error?.value ? 'red' : props.color)
|
||||||
const size = computed(() => formGroup?.size?.value ?? props.size)
|
const size = computed(() => formGroup?.size?.value ?? props.size)
|
||||||
|
const labelFor = formGroup?.labelFor
|
||||||
|
|
||||||
const input = ref<HTMLInputElement | null>(null)
|
const input = ref<HTMLInputElement | null>(null)
|
||||||
|
|
||||||
@@ -255,7 +261,8 @@ export default defineComponent({
|
|||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
attrs: computed(() => omit(attrs, ['class'])),
|
labelFor,
|
||||||
|
attrs: computed(() => omit(attrs, ['class', labelFor ? 'id' : null ])),
|
||||||
// eslint-disable-next-line vue/no-dupe-keys
|
// eslint-disable-next-line vue/no-dupe-keys
|
||||||
ui,
|
ui,
|
||||||
input,
|
input,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div :class="wrapperClass">
|
<div :class="wrapperClass">
|
||||||
<input
|
<input
|
||||||
|
:id="labelFor"
|
||||||
ref="input"
|
ref="input"
|
||||||
v-model.number="value"
|
v-model.number="value"
|
||||||
:name="name"
|
:name="name"
|
||||||
@@ -37,6 +38,10 @@ export default defineComponent({
|
|||||||
type: Number,
|
type: Number,
|
||||||
default: 0
|
default: 0
|
||||||
},
|
},
|
||||||
|
id: {
|
||||||
|
type: String,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
name: {
|
name: {
|
||||||
type: String,
|
type: String,
|
||||||
default: null
|
default: null
|
||||||
@@ -87,9 +92,10 @@ export default defineComponent({
|
|||||||
|
|
||||||
const ui = computed<Partial<typeof appConfig.ui.range>>(() => defuTwMerge({}, props.ui, appConfig.ui.range))
|
const ui = computed<Partial<typeof appConfig.ui.range>>(() => defuTwMerge({}, props.ui, appConfig.ui.range))
|
||||||
|
|
||||||
const { emitFormChange, formGroup } = useFormGroup()
|
const { emitFormChange, formGroup } = useFormGroup(props)
|
||||||
const color = computed(() => formGroup?.error?.value ? 'red' : props.color)
|
const color = computed(() => formGroup?.error?.value ? 'red' : props.color)
|
||||||
const size = computed(() => formGroup?.size?.value ?? props.size)
|
const size = computed(() => formGroup?.size?.value ?? props.size)
|
||||||
|
const labelFor = formGroup?.labelFor
|
||||||
|
|
||||||
const value = computed({
|
const value = computed({
|
||||||
get () {
|
get () {
|
||||||
@@ -161,7 +167,8 @@ export default defineComponent({
|
|||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
attrs: computed(() => omit(attrs, ['class'])),
|
labelFor,
|
||||||
|
attrs: computed(() => omit(attrs, ['class', labelFor ? 'id' : null ])),
|
||||||
// eslint-disable-next-line vue/no-dupe-keys
|
// eslint-disable-next-line vue/no-dupe-keys
|
||||||
ui,
|
ui,
|
||||||
value,
|
value,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div :class="wrapperClass">
|
<div :class="wrapperClass">
|
||||||
<select
|
<select
|
||||||
|
:id="labelFor"
|
||||||
:name="name"
|
:name="name"
|
||||||
:value="modelValue"
|
:value="modelValue"
|
||||||
:required="required"
|
:required="required"
|
||||||
@@ -77,6 +78,10 @@ export default defineComponent({
|
|||||||
type: [String, Number, Object],
|
type: [String, Number, Object],
|
||||||
default: ''
|
default: ''
|
||||||
},
|
},
|
||||||
|
id: {
|
||||||
|
type: String,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
name: {
|
name: {
|
||||||
type: String,
|
type: String,
|
||||||
default: null
|
default: null
|
||||||
@@ -177,9 +182,10 @@ export default defineComponent({
|
|||||||
|
|
||||||
const ui = computed<Partial<typeof appConfig.ui.select>>(() => defuTwMerge({}, props.ui, appConfig.ui.select))
|
const ui = computed<Partial<typeof appConfig.ui.select>>(() => defuTwMerge({}, props.ui, appConfig.ui.select))
|
||||||
|
|
||||||
const { emitFormChange, formGroup } = useFormGroup()
|
const { emitFormChange, formGroup } = useFormGroup(props)
|
||||||
const color = computed(() => formGroup?.error?.value ? 'red' : props.color)
|
const color = computed(() => formGroup?.error?.value ? 'red' : props.color)
|
||||||
const size = computed(() => formGroup?.size?.value ?? props.size)
|
const size = computed(() => formGroup?.size?.value ?? props.size)
|
||||||
|
const labelFor = formGroup?.labelFor
|
||||||
|
|
||||||
|
|
||||||
const onInput = (event: InputEvent) => {
|
const onInput = (event: InputEvent) => {
|
||||||
@@ -318,9 +324,10 @@ export default defineComponent({
|
|||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
attrs: computed(() => omit(attrs, ['class'])),
|
attrs: computed(() => omit(attrs, ['class', labelFor ? 'id' : null ])),
|
||||||
// eslint-disable-next-line vue/no-dupe-keys
|
// eslint-disable-next-line vue/no-dupe-keys
|
||||||
ui,
|
ui,
|
||||||
|
labelFor,
|
||||||
normalizedOptionsWithPlaceholder,
|
normalizedOptionsWithPlaceholder,
|
||||||
normalizedValue,
|
normalizedValue,
|
||||||
isLeading,
|
isLeading,
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
class="inline-flex w-full"
|
class="inline-flex w-full"
|
||||||
>
|
>
|
||||||
<slot :open="open" :disabled="disabled" :loading="loading">
|
<slot :open="open" :disabled="disabled" :loading="loading">
|
||||||
<button :class="selectClass" :disabled="disabled || loading" type="button" v-bind="attrs">
|
<button :id="labelFor" :class="selectClass" :disabled="disabled || loading" type="button" v-bind="attrs">
|
||||||
<span v-if="(isLeading && leadingIconName) || $slots.leading" :class="leadingWrapperIconClass">
|
<span v-if="(isLeading && leadingIconName) || $slots.leading" :class="leadingWrapperIconClass">
|
||||||
<slot name="leading" :disabled="disabled" :loading="loading">
|
<slot name="leading" :disabled="disabled" :loading="loading">
|
||||||
<UIcon :name="leadingIconName" :class="leadingIconClass" />
|
<UIcon :name="leadingIconName" :class="leadingIconClass" />
|
||||||
@@ -174,6 +174,10 @@ export default defineComponent({
|
|||||||
type: Array as PropType<{ [key: string]: any, disabled?: boolean }[] | string[]>,
|
type: Array as PropType<{ [key: string]: any, disabled?: boolean }[] | string[]>,
|
||||||
default: () => []
|
default: () => []
|
||||||
},
|
},
|
||||||
|
id: {
|
||||||
|
type: String,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
name: {
|
name: {
|
||||||
type: String,
|
type: String,
|
||||||
default: null
|
default: null
|
||||||
@@ -310,9 +314,10 @@ export default defineComponent({
|
|||||||
const popper = computed<PopperOptions>(() => defu({}, props.popper, uiMenu.value.popper as PopperOptions))
|
const popper = computed<PopperOptions>(() => defu({}, props.popper, uiMenu.value.popper as PopperOptions))
|
||||||
|
|
||||||
const [trigger, container] = usePopper(popper.value)
|
const [trigger, container] = usePopper(popper.value)
|
||||||
const { emitFormBlur, emitFormChange, formGroup } = useFormGroup()
|
const { emitFormBlur, emitFormChange, formGroup } = useFormGroup(props)
|
||||||
const color = computed(() => formGroup?.error?.value ? 'red' : props.color)
|
const color = computed(() => formGroup?.error?.value ? 'red' : props.color)
|
||||||
const size = computed(() => formGroup?.size?.value ?? props.size)
|
const size = computed(() => formGroup?.size?.value ?? props.size)
|
||||||
|
const labelFor = formGroup?.labelFor
|
||||||
|
|
||||||
const query = ref('')
|
const query = ref('')
|
||||||
const searchInput = ref<ComponentPublicInstance<HTMLElement>>()
|
const searchInput = ref<ComponentPublicInstance<HTMLElement>>()
|
||||||
@@ -437,7 +442,8 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
attrs: computed(() => omit(attrs, ['class'])),
|
labelFor,
|
||||||
|
attrs: computed(() => omit(attrs, ['class', labelFor ? 'id' : null ])),
|
||||||
// eslint-disable-next-line vue/no-dupe-keys
|
// eslint-disable-next-line vue/no-dupe-keys
|
||||||
uiMenu,
|
uiMenu,
|
||||||
trigger,
|
trigger,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div :class="wrapperClass">
|
<div :class="wrapperClass">
|
||||||
<textarea
|
<textarea
|
||||||
|
:id="labelFor"
|
||||||
ref="textarea"
|
ref="textarea"
|
||||||
:value="modelValue"
|
:value="modelValue"
|
||||||
:name="name"
|
:name="name"
|
||||||
@@ -38,6 +39,10 @@ export default defineComponent({
|
|||||||
type: [String, Number],
|
type: [String, Number],
|
||||||
default: ''
|
default: ''
|
||||||
},
|
},
|
||||||
|
id: {
|
||||||
|
type: String,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
name: {
|
name: {
|
||||||
type: String,
|
type: String,
|
||||||
default: null
|
default: null
|
||||||
@@ -116,9 +121,10 @@ export default defineComponent({
|
|||||||
|
|
||||||
const ui = computed<Partial<typeof appConfig.ui.textarea>>(() => defuTwMerge({}, props.ui, appConfig.ui.textarea))
|
const ui = computed<Partial<typeof appConfig.ui.textarea>>(() => defuTwMerge({}, props.ui, appConfig.ui.textarea))
|
||||||
|
|
||||||
const { emitFormBlur, emitFormInput, formGroup } = useFormGroup()
|
const { emitFormBlur, emitFormInput, formGroup } = useFormGroup(props)
|
||||||
const color = computed(() => formGroup?.error?.value ? 'red' : props.color)
|
const color = computed(() => formGroup?.error?.value ? 'red' : props.color)
|
||||||
const size = computed(() => formGroup?.size?.value ?? props.size)
|
const size = computed(() => formGroup?.size?.value ?? props.size)
|
||||||
|
const labelFor = formGroup?.labelFor
|
||||||
|
|
||||||
const autoFocus = () => {
|
const autoFocus = () => {
|
||||||
if (props.autofocus) {
|
if (props.autofocus) {
|
||||||
@@ -194,9 +200,8 @@ export default defineComponent({
|
|||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
attrs: computed(() => omit(attrs, ['class'])),
|
labelFor,
|
||||||
// eslint-disable-next-line vue/no-dupe-keys
|
attrs: computed(() => omit(attrs, ['class', labelFor ? 'id' : null ])),
|
||||||
ui,
|
|
||||||
textarea,
|
textarea,
|
||||||
wrapperClass,
|
wrapperClass,
|
||||||
// eslint-disable-next-line vue/no-dupe-keys
|
// eslint-disable-next-line vue/no-dupe-keys
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<HSwitch
|
<HSwitch
|
||||||
|
:id="labelFor"
|
||||||
v-model="active"
|
v-model="active"
|
||||||
:name="name"
|
:name="name"
|
||||||
:disabled="disabled"
|
:disabled="disabled"
|
||||||
@@ -40,6 +41,10 @@ export default defineComponent({
|
|||||||
},
|
},
|
||||||
inheritAttrs: false,
|
inheritAttrs: false,
|
||||||
props: {
|
props: {
|
||||||
|
id: {
|
||||||
|
type: String,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
name: {
|
name: {
|
||||||
type: String,
|
type: String,
|
||||||
default: null
|
default: null
|
||||||
@@ -79,8 +84,9 @@ export default defineComponent({
|
|||||||
|
|
||||||
const ui = computed<Partial<typeof appConfig.ui.toggle>>(() => defuTwMerge({}, props.ui, appConfig.ui.toggle))
|
const ui = computed<Partial<typeof appConfig.ui.toggle>>(() => defuTwMerge({}, props.ui, appConfig.ui.toggle))
|
||||||
|
|
||||||
const { emitFormChange, formGroup } = useFormGroup()
|
const { emitFormChange, formGroup } = useFormGroup(props)
|
||||||
const color = computed(() => formGroup?.error?.value ? 'red' : props.color)
|
const color = computed(() => formGroup?.error?.value ? 'red' : props.color)
|
||||||
|
const labelFor = formGroup?.labelFor
|
||||||
|
|
||||||
const active = computed({
|
const active = computed({
|
||||||
get () {
|
get () {
|
||||||
@@ -114,7 +120,8 @@ export default defineComponent({
|
|||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
attrs: computed(() => omit(attrs, ['class'])),
|
labelFor,
|
||||||
|
attrs: computed(() => omit(attrs, ['class', labelFor ? 'id' : null ])),
|
||||||
// eslint-disable-next-line vue/no-dupe-keys
|
// eslint-disable-next-line vue/no-dupe-keys
|
||||||
ui,
|
ui,
|
||||||
active,
|
active,
|
||||||
|
|||||||
@@ -1,10 +1,18 @@
|
|||||||
import { inject, ref } from 'vue'
|
import { inject, ref } from 'vue'
|
||||||
import { type UseEventBusReturn, useDebounceFn } from '@vueuse/core'
|
import { type UseEventBusReturn, useDebounceFn } from '@vueuse/core'
|
||||||
import type { FormEvent, FormEventType } from '../types/form'
|
import type { FormEvent, FormEventType, InjectedFormGroupValue } from '../types/form'
|
||||||
|
|
||||||
export const useFormGroup = () => {
|
type InputAttrs = {
|
||||||
|
id?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useFormGroup = (inputAttrs?: InputAttrs) => {
|
||||||
const formBus = inject<UseEventBusReturn<FormEvent, string> | undefined>('form-events', undefined)
|
const formBus = inject<UseEventBusReturn<FormEvent, string> | undefined>('form-events', undefined)
|
||||||
const formGroup = inject('form-group', undefined)
|
const formGroup = inject<InjectedFormGroupValue>('form-group', undefined)
|
||||||
|
|
||||||
|
if (formGroup) {
|
||||||
|
formGroup.labelFor.value = inputAttrs?.id ?? formGroup?.labelFor.value
|
||||||
|
}
|
||||||
|
|
||||||
const blurred = ref(false)
|
const blurred = ref(false)
|
||||||
|
|
||||||
|
|||||||
7
src/runtime/types/form.d.ts
vendored
7
src/runtime/types/form.d.ts
vendored
@@ -19,3 +19,10 @@ export interface FormEvent {
|
|||||||
type: FormEventType
|
type: FormEventType
|
||||||
path: string
|
path: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface InjectedFormGroupValue {
|
||||||
|
labelFor: Ref<string>
|
||||||
|
name: Ref<string>
|
||||||
|
size: Ref<string>
|
||||||
|
error: Ref<string | boolean>
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user