From f9aaa9995acbec8a903f298a579af666cae8ad06 Mon Sep 17 00:00:00 2001 From: rdjanuar Date: Wed, 16 Jul 2025 11:33:03 +0700 Subject: [PATCH 01/16] feat(FormField): add required field support across form components --- playground/app/pages/components/form.vue | 8 ++++---- src/runtime/components/Checkbox.vue | 7 +++++-- src/runtime/components/CheckboxGroup.vue | 8 ++++++-- src/runtime/components/FormField.vue | 1 + src/runtime/components/Input.vue | 6 ++++-- src/runtime/components/InputMenu.vue | 7 +++++-- src/runtime/components/InputNumber.vue | 4 +++- src/runtime/components/InputTags.vue | 6 ++++-- src/runtime/components/PinInput.vue | 7 +++++-- src/runtime/components/RadioGroup.vue | 7 +++++-- src/runtime/components/Select.vue | 6 ++++-- src/runtime/components/SelectMenu.vue | 6 ++++-- src/runtime/components/Slider.vue | 5 ++++- src/runtime/components/Switch.vue | 6 ++++-- src/runtime/components/Textarea.vue | 5 +++-- src/runtime/composables/useFormField.ts | 1 + src/runtime/types/form.ts | 1 + 17 files changed, 63 insertions(+), 28 deletions(-) diff --git a/playground/app/pages/components/form.vue b/playground/app/pages/components/form.vue index 9641ce79d0..01fc0e7086 100644 --- a/playground/app/pages/components/form.vue +++ b/playground/app/pages/components/form.vue @@ -32,15 +32,15 @@ const disabled = ref(false) class="gap-4 flex flex-col w-60" @submit="onSubmit" > - + - + - + @@ -56,7 +56,7 @@ const disabled = ref(false)
- + diff --git a/src/runtime/components/Checkbox.vue b/src/runtime/components/Checkbox.vue index 95029b8d99..c85cc687d9 100644 --- a/src/runtime/components/Checkbox.vue +++ b/src/runtime/components/Checkbox.vue @@ -76,11 +76,13 @@ const modelValue = defineModel({ default: undefined } const appConfig = useAppConfig() as Checkbox['AppConfig'] -const rootProps = useForwardProps(reactivePick(props, 'required', 'value', 'defaultValue')) +const rootProps = useForwardProps(reactivePick(props, 'value', 'defaultValue')) -const { id: _id, emitFormChange, emitFormInput, size, color, name, disabled, ariaAttrs } = useFormField(props) +const { id: _id, emitFormChange, emitFormInput, size, color, name, disabled, ariaAttrs, required: formFieldRequired } = useFormField(props) const id = _id.value ?? useId() +const decideRequired = computed(() => props.required || formFieldRequired.value) + const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.checkbox || {}) })({ size: size.value, color: color.value, @@ -108,6 +110,7 @@ function onUpdate(value: any) { v-bind="{ ...rootProps, ...$attrs, ...ariaAttrs }" v-model="modelValue" :name="name" + :required="decideRequired" :disabled="disabled" :class="ui.base({ class: props.ui?.base })" @update:model-value="onUpdate" diff --git a/src/runtime/components/CheckboxGroup.vue b/src/runtime/components/CheckboxGroup.vue index dd997c2401..01511f6271 100644 --- a/src/runtime/components/CheckboxGroup.vue +++ b/src/runtime/components/CheckboxGroup.vue @@ -93,11 +93,14 @@ const slots = defineSlots>() const appConfig = useAppConfig() as CheckboxGroup['AppConfig'] -const rootProps = useForwardPropsEmits(reactivePick(props, 'as', 'modelValue', 'defaultValue', 'orientation', 'loop', 'required'), emits) +const rootProps = useForwardPropsEmits(reactivePick(props, 'as', 'modelValue', 'defaultValue', 'orientation', 'loop'), emits) const checkboxProps = useForwardProps(reactivePick(props, 'variant', 'indicator', 'icon')) const proxySlots = omit(slots, ['legend']) -const { emitFormChange, emitFormInput, color, name, size, id: _id, disabled, ariaAttrs } = useFormField>(props, { bind: false }) +const { emitFormChange, emitFormInput, color, name, size, id: _id, disabled, ariaAttrs, required: formFieldRequired } = useFormField>(props, { bind: false }) + +const decideRequired = computed(() => props.required || formFieldRequired.value) + const id = _id.value ?? useId() const ui = computed(() => tv({ extend: theme, ...(appConfig.ui?.checkboxGroup || {}) })({ @@ -159,6 +162,7 @@ function onUpdate(value: any) { ({ name: props.name, size: props.size, eagerValidation: props.eagerValidation, + required: props.required, validateOnInputDelay: props.validateOnInputDelay, errorPattern: props.errorPattern, hint: props.hint, diff --git a/src/runtime/components/Input.vue b/src/runtime/components/Input.vue index 52c1c52661..6bd7f6c7bb 100644 --- a/src/runtime/components/Input.vue +++ b/src/runtime/components/Input.vue @@ -91,12 +91,14 @@ const modelValue = useVModel, 'modelValue', 'update:modelValue'>(p const appConfig = useAppConfig() as Input['AppConfig'] -const { emitFormBlur, emitFormInput, emitFormChange, size: formGroupSize, color, id, name, highlight, disabled, emitFormFocus, ariaAttrs } = useFormField>(props, { deferInputValidation: true }) +const { emitFormBlur, emitFormInput, emitFormChange, size: formGroupSize, color, id, name, highlight, disabled, emitFormFocus, ariaAttrs, required: formFieldRequired } = useFormField>(props, { deferInputValidation: true }) const { orientation, size: buttonGroupSize } = useButtonGroup>(props) const { isLeading, isTrailing, leadingIconName, trailingIconName } = useComponentIcons(props) const inputSize = computed(() => buttonGroupSize.value || formGroupSize.value) +const decideRequired = computed(() => props.required || formFieldRequired.value) + const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.input || {}) })({ type: props.type as Input['variants']['type'], color: color.value, @@ -184,7 +186,7 @@ defineExpose({ :placeholder="placeholder" :class="ui.base({ class: props.ui?.base })" :disabled="disabled" - :required="required" + :required="decideRequired" :autocomplete="autocomplete" v-bind="{ ...$attrs, ...ariaAttrs }" @input="onInput" diff --git a/src/runtime/components/InputMenu.vue b/src/runtime/components/InputMenu.vue index c2a64ce62c..3f365a8a0c 100644 --- a/src/runtime/components/InputMenu.vue +++ b/src/runtime/components/InputMenu.vue @@ -207,17 +207,19 @@ const { t } = useLocale() const appConfig = useAppConfig() as InputMenu['AppConfig'] const { contains } = useFilter({ sensitivity: 'base' }) -const rootProps = useForwardPropsEmits(reactivePick(props, 'as', 'modelValue', 'defaultValue', 'open', 'defaultOpen', 'required', 'multiple', 'resetSearchTermOnBlur', 'resetSearchTermOnSelect', 'highlightOnHover', 'ignoreFilter'), emits) +const rootProps = useForwardPropsEmits(reactivePick(props, 'as', 'modelValue', 'defaultValue', 'open', 'defaultOpen', 'multiple', 'resetSearchTermOnBlur', 'resetSearchTermOnSelect', 'highlightOnHover', 'ignoreFilter'), emits) const portalProps = usePortal(toRef(() => props.portal)) const contentProps = toRef(() => defu(props.content, { side: 'bottom', sideOffset: 8, collisionPadding: 8, position: 'popper' }) as ComboboxContentProps) const arrowProps = toRef(() => props.arrow as ComboboxArrowProps) -const { emitFormBlur, emitFormFocus, emitFormChange, emitFormInput, size: formGroupSize, color, id, name, highlight, disabled, ariaAttrs } = useFormField(props) +const { emitFormBlur, emitFormFocus, emitFormChange, emitFormInput, size: formGroupSize, color, id, name, highlight, disabled, ariaAttrs, required: formFieldRequired } = useFormField(props) const { orientation, size: buttonGroupSize } = useButtonGroup(props) const { isLeading, isTrailing, leadingIconName, trailingIconName } = useComponentIcons(toRef(() => defu(props, { trailingIcon: appConfig.ui.icons.chevronDown }))) const inputSize = computed(() => buttonGroupSize.value || formGroupSize.value) +const decideRequired = computed(() => props.required || formFieldRequired.value) + const [DefineCreateItemTemplate, ReuseCreateItemTemplate] = createReusableTemplate() const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.inputMenu || {}) })({ @@ -408,6 +410,7 @@ defineExpose({ v-slot="{ modelValue, open }" v-bind="rootProps" :name="name" + :required="decideRequired" :disabled="disabled" :class="ui.root({ class: [props.ui?.root, props.class] })" :as-child="!!multiple" diff --git a/src/runtime/components/InputNumber.vue b/src/runtime/components/InputNumber.vue index e212122d06..ea50cae8c0 100644 --- a/src/runtime/components/InputNumber.vue +++ b/src/runtime/components/InputNumber.vue @@ -100,11 +100,12 @@ const appConfig = useAppConfig() as InputNumber['AppConfig'] const rootProps = useForwardPropsEmits(reactivePick(props, 'as', 'modelValue', 'defaultValue', 'min', 'max', 'step', 'stepSnapping', 'formatOptions', 'disableWheelChange', 'invertWheelChange'), emits) -const { emitFormBlur, emitFormFocus, emitFormChange, emitFormInput, id, color, size: formGroupSize, name, highlight, disabled, ariaAttrs } = useFormField(props) +const { emitFormBlur, emitFormFocus, emitFormChange, emitFormInput, id, color, size: formGroupSize, name, highlight, disabled, ariaAttrs, required: formFieldRequired } = useFormField(props) const { orientation, size: buttonGroupSize } = useButtonGroup(props) const locale = computed(() => props.locale || codeLocale.value) const inputSize = computed(() => buttonGroupSize.value || formGroupSize.value) +const decideRequired = computed(() => props.required || formFieldRequired.value) const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.inputNumber || {}) })({ color: color.value, @@ -157,6 +158,7 @@ defineExpose({ :id="id" :class="ui.root({ class: [props.ui?.root, props.class] })" :name="name" + :required="decideRequired" :disabled="disabled" :locale="locale" @update:model-value="onUpdate" diff --git a/src/runtime/components/InputTags.vue b/src/runtime/components/InputTags.vue index 973d3dfb00..189f96eca5 100644 --- a/src/runtime/components/InputTags.vue +++ b/src/runtime/components/InputTags.vue @@ -86,13 +86,14 @@ const slots = defineSlots>() const appConfig = useAppConfig() as InputTags['AppConfig'] -const rootProps = useForwardPropsEmits(reactivePick(props, 'as', 'addOnPaste', 'addOnTab', 'addOnBlur', 'duplicate', 'delimiter', 'max', 'convertValue', 'displayValue', 'required'), emits) +const rootProps = useForwardPropsEmits(reactivePick(props, 'as', 'addOnPaste', 'addOnTab', 'addOnBlur', 'duplicate', 'delimiter', 'max', 'convertValue', 'displayValue'), emits) -const { emitFormBlur, emitFormFocus, emitFormChange, emitFormInput, size: formGroupSize, color, id, name, highlight, disabled, ariaAttrs } = useFormField(props) +const { emitFormBlur, emitFormFocus, emitFormChange, emitFormInput, size: formGroupSize, color, id, name, highlight, disabled, ariaAttrs, required: formFieldRequired } = useFormField(props) const { orientation, size: buttonGroupSize } = useButtonGroup(props) const { isLeading, isTrailing, leadingIconName, trailingIconName } = useComponentIcons(props) const inputSize = computed(() => buttonGroupSize.value || formGroupSize.value) +const decideRequired = computed(() => props.required || formFieldRequired.value) const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.inputTags || {}) })({ color: color.value, @@ -154,6 +155,7 @@ defineExpose({ :default-value="defaultValue" :class="ui.root({ class: [ui.base({ class: props.ui?.base }), props.ui?.root, props.class] })" v-bind="rootProps" + :required="decideRequired" :name="name" :disabled="disabled" @update:model-value="onUpdate" diff --git a/src/runtime/components/PinInput.vue b/src/runtime/components/PinInput.vue index d90e698a2f..c31ad6213d 100644 --- a/src/runtime/components/PinInput.vue +++ b/src/runtime/components/PinInput.vue @@ -65,9 +65,11 @@ const emits = defineEmits>() const appConfig = useAppConfig() as PinInput['AppConfig'] -const rootProps = useForwardPropsEmits(reactivePick(props, 'defaultValue', 'disabled', 'id', 'mask', 'modelValue', 'name', 'otp', 'required', 'type'), emits) +const rootProps = useForwardPropsEmits(reactivePick(props, 'defaultValue', 'disabled', 'id', 'mask', 'modelValue', 'name', 'otp', 'type'), emits) -const { emitFormInput, emitFormFocus, emitFormChange, emitFormBlur, size, color, id, name, highlight, disabled, ariaAttrs } = useFormField(props) +const { emitFormInput, emitFormFocus, emitFormChange, emitFormBlur, size, color, id, name, highlight, disabled, ariaAttrs, required: formFieldRequired } = useFormField(props) + +const decideRequired = computed(() => props.required || formFieldRequired.value) const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.pinInput || {}) })({ color: color.value, @@ -115,6 +117,7 @@ defineExpose({ v-bind="{ ...rootProps, ...ariaAttrs }" :id="id" :name="name" + :required="decideRequired" :placeholder="placeholder" :class="ui.root({ class: [props.ui?.root, props.class] })" @update:model-value="emitFormInput()" diff --git a/src/runtime/components/RadioGroup.vue b/src/runtime/components/RadioGroup.vue index 3a07201ff9..8ca444e9ba 100644 --- a/src/runtime/components/RadioGroup.vue +++ b/src/runtime/components/RadioGroup.vue @@ -99,9 +99,9 @@ const slots = defineSlots>() const appConfig = useAppConfig() as RadioGroup['AppConfig'] -const rootProps = useForwardPropsEmits(reactivePick(props, 'as', 'modelValue', 'defaultValue', 'orientation', 'loop', 'required'), emits) +const rootProps = useForwardPropsEmits(reactivePick(props, 'as', 'modelValue', 'defaultValue', 'orientation', 'loop'), emits) -const { emitFormChange, emitFormInput, color, name, size, id: _id, disabled, ariaAttrs } = useFormField>(props, { bind: false }) +const { emitFormChange, emitFormInput, color, name, size, id: _id, disabled, ariaAttrs, required: formFieldRequired } = useFormField>(props, { bind: false }) const id = _id.value ?? useId() const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.radioGroup || {}) })({ @@ -113,6 +113,7 @@ const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.radioGroup | variant: props.variant, indicator: props.indicator })) +const decideRequired = computed(() => props.required || formFieldRequired.value) function normalizeItem(item: any) { if (item === null) { @@ -166,6 +167,8 @@ function onUpdate(value: any) { :id="id" v-slot="{ modelValue }" v-bind="rootProps" + :required="decideRequired" + :name="name" :disabled="disabled" :class="ui.root({ class: [props.ui?.root, props.class] })" diff --git a/src/runtime/components/Select.vue b/src/runtime/components/Select.vue index 09268beb98..9870b1b3f7 100644 --- a/src/runtime/components/Select.vue +++ b/src/runtime/components/Select.vue @@ -164,14 +164,15 @@ const slots = defineSlots>() const appConfig = useAppConfig() as Select['AppConfig'] -const rootProps = useForwardPropsEmits(reactivePick(props, 'open', 'defaultOpen', 'disabled', 'autocomplete', 'required', 'multiple'), emits) +const rootProps = useForwardPropsEmits(reactivePick(props, 'open', 'defaultOpen', 'disabled', 'autocomplete', 'multiple'), emits) const portalProps = usePortal(toRef(() => props.portal)) const contentProps = toRef(() => defu(props.content, { side: 'bottom', sideOffset: 8, collisionPadding: 8, position: 'popper' }) as SelectContentProps) const arrowProps = toRef(() => props.arrow as SelectArrowProps) -const { emitFormChange, emitFormInput, emitFormBlur, emitFormFocus, size: formGroupSize, color, id, name, highlight, disabled, ariaAttrs } = useFormField(props) +const { emitFormChange, emitFormInput, emitFormBlur, emitFormFocus, size: formGroupSize, color, id, name, highlight, disabled, ariaAttrs, required: formFieldRequired } = useFormField(props) const { orientation, size: buttonGroupSize } = useButtonGroup(props) const { isLeading, isTrailing, leadingIconName, trailingIconName } = useComponentIcons(toRef(() => defu(props, { trailingIcon: appConfig.ui.icons.chevronDown }))) +const decideRequired = computed(() => props.required || formFieldRequired.value) const selectSize = computed(() => buttonGroupSize.value || formGroupSize.value) @@ -257,6 +258,7 @@ defineExpose({ v-slot="{ modelValue, open }" :name="name" v-bind="rootProps" + :required="decideRequired" :autocomplete="autocomplete" :disabled="disabled" :default-value="(defaultValue as (AcceptableValue | AcceptableValue[]))" diff --git a/src/runtime/components/SelectMenu.vue b/src/runtime/components/SelectMenu.vue index 9753b312ca..33f6d03c36 100644 --- a/src/runtime/components/SelectMenu.vue +++ b/src/runtime/components/SelectMenu.vue @@ -203,17 +203,18 @@ const { t } = useLocale() const appConfig = useAppConfig() as SelectMenu['AppConfig'] const { contains } = useFilter({ sensitivity: 'base' }) -const rootProps = useForwardPropsEmits(reactivePick(props, 'modelValue', 'defaultValue', 'open', 'defaultOpen', 'required', 'multiple', 'resetSearchTermOnBlur', 'resetSearchTermOnSelect', 'highlightOnHover'), emits) +const rootProps = useForwardPropsEmits(reactivePick(props, 'modelValue', 'defaultValue', 'open', 'defaultOpen', 'multiple', 'resetSearchTermOnBlur', 'resetSearchTermOnSelect', 'highlightOnHover'), emits) const portalProps = usePortal(toRef(() => props.portal)) const contentProps = toRef(() => defu(props.content, { side: 'bottom', sideOffset: 8, collisionPadding: 8, position: 'popper' }) as ComboboxContentProps) const arrowProps = toRef(() => props.arrow as ComboboxArrowProps) const searchInputProps = toRef(() => defu(props.searchInput, { placeholder: t('selectMenu.search'), variant: 'none' }) as InputProps) -const { emitFormBlur, emitFormFocus, emitFormInput, emitFormChange, size: formGroupSize, color, id, name, highlight, disabled, ariaAttrs } = useFormField(props) +const { emitFormBlur, emitFormFocus, emitFormInput, emitFormChange, size: formGroupSize, color, id, name, highlight, disabled, ariaAttrs, required: formFieldRequired } = useFormField(props) const { orientation, size: buttonGroupSize } = useButtonGroup(props) const { isLeading, isTrailing, leadingIconName, trailingIconName } = useComponentIcons(toRef(() => defu(props, { trailingIcon: appConfig.ui.icons.chevronDown }))) const selectSize = computed(() => buttonGroupSize.value || formGroupSize.value) +const decideRequired = computed(() => props.required || formFieldRequired.value) const [DefineCreateItemTemplate, ReuseCreateItemTemplate] = createReusableTemplate() @@ -392,6 +393,7 @@ defineExpose({ v-slot="{ modelValue, open }" v-bind="{ ...rootProps, ...$attrs, ...ariaAttrs }" ignore-filter + :required="decideRequired" as-child :name="name" :disabled="disabled" diff --git a/src/runtime/components/Slider.vue b/src/runtime/components/Slider.vue index f9d86fefc5..ac8a606c8f 100644 --- a/src/runtime/components/Slider.vue +++ b/src/runtime/components/Slider.vue @@ -34,6 +34,7 @@ export interface SliderProps extends Pick(props) +const { id, emitFormChange, emitFormInput, size, color, name, disabled, ariaAttrs, required: formFieldRequired } = useFormField(props) +const decideRequired = computed(() => props.required || formFieldRequired.value) const defaultSliderValue = computed(() => { if (typeof props.defaultValue === 'number') { return [props.defaultValue] @@ -112,6 +114,7 @@ function onChange(value: any) { v-model="sliderValue" :name="name" :disabled="disabled" + :required="decideRequired" :class="ui.root({ class: [props.ui?.root, props.class] })" :default-value="defaultSliderValue" @update:model-value="emitFormInput()" diff --git a/src/runtime/components/Switch.vue b/src/runtime/components/Switch.vue index 541fa8d42a..bc27838116 100644 --- a/src/runtime/components/Switch.vue +++ b/src/runtime/components/Switch.vue @@ -73,9 +73,9 @@ const modelValue = defineModel({ default: undefined }) const appConfig = useAppConfig() as Switch['AppConfig'] -const rootProps = useForwardProps(reactivePick(props, 'required', 'value', 'defaultValue')) +const rootProps = useForwardProps(reactivePick(props, 'value', 'defaultValue')) -const { id: _id, emitFormChange, emitFormInput, size, color, name, disabled, ariaAttrs } = useFormField(props) +const { id: _id, emitFormChange, emitFormInput, size, color, name, disabled, ariaAttrs, required: formFieldRequired } = useFormField(props) const id = _id.value ?? useId() const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.switch || {}) })({ @@ -85,6 +85,7 @@ const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.switch || {} loading: props.loading, disabled: disabled.value || props.loading })) +const decideRequired = computed(() => props.required || formFieldRequired.value) function onUpdate(value: any) { // @ts-expect-error - 'target' does not exist in type 'EventInit' @@ -102,6 +103,7 @@ function onUpdate(value: any) { :id="id" v-bind="{ ...rootProps, ...$attrs, ...ariaAttrs }" v-model="modelValue" + :required="decideRequired" :name="name" :disabled="disabled || loading" :class="ui.base({ class: props.ui?.base })" diff --git a/src/runtime/components/Textarea.vue b/src/runtime/components/Textarea.vue index 4fe634ed67..7cbb87d974 100644 --- a/src/runtime/components/Textarea.vue +++ b/src/runtime/components/Textarea.vue @@ -94,7 +94,7 @@ const modelValue = useVModel, 'modelValue', 'update:modelValue' const appConfig = useAppConfig() as Textarea['AppConfig'] -const { emitFormFocus, emitFormBlur, emitFormInput, emitFormChange, size, color, id, name, highlight, disabled, ariaAttrs } = useFormField>(props, { deferInputValidation: true }) +const { emitFormFocus, emitFormBlur, emitFormInput, emitFormChange, size, color, id, name, highlight, disabled, ariaAttrs, required: formFieldRequired } = useFormField>(props, { deferInputValidation: true }) const { isLeading, isTrailing, leadingIconName, trailingIconName } = useComponentIcons(props) const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.textarea || {}) })({ @@ -107,6 +107,7 @@ const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.textarea || leading: isLeading.value || !!props.avatar || !!slots.leading, trailing: isTrailing.value || !!slots.trailing })) +const decideRequired = computed(() => props.required || formFieldRequired.value) const textareaRef = ref(null) @@ -215,7 +216,7 @@ defineExpose({ :placeholder="placeholder" :class="ui.base({ class: props.ui?.base })" :disabled="disabled" - :required="required" + :required="decideRequired" v-bind="{ ...$attrs, ...ariaAttrs }" @input="onInput" @blur="onBlur" diff --git a/src/runtime/composables/useFormField.ts b/src/runtime/composables/useFormField.ts index 7a5328ff1e..01458d633b 100644 --- a/src/runtime/composables/useFormField.ts +++ b/src/runtime/composables/useFormField.ts @@ -77,6 +77,7 @@ export function useFormField(props?: Props, opts?: { bind?: boolean, defer size: computed(() => props?.size ?? formField?.value.size), color: computed(() => formField?.value.error ? 'error' : props?.color), highlight: computed(() => formField?.value.error ? true : props?.highlight), + required: computed(() => formField?.value.required), disabled: computed(() => formOptions?.value.disabled || props?.disabled), emitFormBlur, emitFormInput, diff --git a/src/runtime/types/form.ts b/src/runtime/types/form.ts index fb04d7dcf9..e65a56c27a 100644 --- a/src/runtime/types/form.ts +++ b/src/runtime/types/form.ts @@ -100,6 +100,7 @@ export interface FormFieldInjectedOptions { eagerValidation?: boolean validateOnInputDelay?: number errorPattern?: RegExp + required?: boolean hint?: string description?: string help?: string From 6889618db60258564cf960ec1aeed65d74cd54d0 Mon Sep 17 00:00:00 2001 From: rdjanuar Date: Wed, 16 Jul 2025 17:10:18 +0700 Subject: [PATCH 02/16] fix(form): simplify required field logic by moving it to useFormField --- playground/app/pages/components/form.vue | 8 ++++---- src/runtime/components/Checkbox.vue | 6 ++---- src/runtime/components/CheckboxGroup.vue | 6 ++---- src/runtime/components/Input.vue | 6 ++---- src/runtime/components/InputMenu.vue | 6 ++---- src/runtime/components/InputNumber.vue | 5 ++--- src/runtime/components/InputTags.vue | 5 ++--- src/runtime/components/PinInput.vue | 6 ++---- src/runtime/components/RadioGroup.vue | 5 ++--- src/runtime/components/Select.vue | 5 ++--- src/runtime/components/SelectMenu.vue | 5 ++--- src/runtime/components/Slider.vue | 5 ++--- src/runtime/components/Switch.vue | 5 ++--- src/runtime/components/Textarea.vue | 5 ++--- src/runtime/composables/useFormField.ts | 3 ++- 15 files changed, 32 insertions(+), 49 deletions(-) diff --git a/playground/app/pages/components/form.vue b/playground/app/pages/components/form.vue index 01fc0e7086..9641ce79d0 100644 --- a/playground/app/pages/components/form.vue +++ b/playground/app/pages/components/form.vue @@ -32,15 +32,15 @@ const disabled = ref(false) class="gap-4 flex flex-col w-60" @submit="onSubmit" > - + - + - + @@ -56,7 +56,7 @@ const disabled = ref(false)
- + diff --git a/src/runtime/components/Checkbox.vue b/src/runtime/components/Checkbox.vue index c85cc687d9..59ba2d3837 100644 --- a/src/runtime/components/Checkbox.vue +++ b/src/runtime/components/Checkbox.vue @@ -78,11 +78,9 @@ const appConfig = useAppConfig() as Checkbox['AppConfig'] const rootProps = useForwardProps(reactivePick(props, 'value', 'defaultValue')) -const { id: _id, emitFormChange, emitFormInput, size, color, name, disabled, ariaAttrs, required: formFieldRequired } = useFormField(props) +const { id: _id, emitFormChange, emitFormInput, size, color, name, disabled, ariaAttrs, required } = useFormField(props) const id = _id.value ?? useId() -const decideRequired = computed(() => props.required || formFieldRequired.value) - const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.checkbox || {}) })({ size: size.value, color: color.value, @@ -110,7 +108,7 @@ function onUpdate(value: any) { v-bind="{ ...rootProps, ...$attrs, ...ariaAttrs }" v-model="modelValue" :name="name" - :required="decideRequired" + :required="required" :disabled="disabled" :class="ui.base({ class: props.ui?.base })" @update:model-value="onUpdate" diff --git a/src/runtime/components/CheckboxGroup.vue b/src/runtime/components/CheckboxGroup.vue index 01511f6271..b1a5c1cafc 100644 --- a/src/runtime/components/CheckboxGroup.vue +++ b/src/runtime/components/CheckboxGroup.vue @@ -97,9 +97,7 @@ const rootProps = useForwardPropsEmits(reactivePick(props, 'as', 'modelValue', ' const checkboxProps = useForwardProps(reactivePick(props, 'variant', 'indicator', 'icon')) const proxySlots = omit(slots, ['legend']) -const { emitFormChange, emitFormInput, color, name, size, id: _id, disabled, ariaAttrs, required: formFieldRequired } = useFormField>(props, { bind: false }) - -const decideRequired = computed(() => props.required || formFieldRequired.value) +const { emitFormChange, emitFormInput, color, name, size, id: _id, disabled, ariaAttrs, required } = useFormField>(props, { bind: false }) const id = _id.value ?? useId() @@ -162,7 +160,7 @@ function onUpdate(value: any) { , 'modelValue', 'update:modelValue'>(p const appConfig = useAppConfig() as Input['AppConfig'] -const { emitFormBlur, emitFormInput, emitFormChange, size: formGroupSize, color, id, name, highlight, disabled, emitFormFocus, ariaAttrs, required: formFieldRequired } = useFormField>(props, { deferInputValidation: true }) +const { emitFormBlur, emitFormInput, emitFormChange, size: formGroupSize, color, id, name, highlight, disabled, emitFormFocus, ariaAttrs, required } = useFormField>(props, { deferInputValidation: true }) const { orientation, size: buttonGroupSize } = useButtonGroup>(props) const { isLeading, isTrailing, leadingIconName, trailingIconName } = useComponentIcons(props) const inputSize = computed(() => buttonGroupSize.value || formGroupSize.value) -const decideRequired = computed(() => props.required || formFieldRequired.value) - const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.input || {}) })({ type: props.type as Input['variants']['type'], color: color.value, @@ -186,7 +184,7 @@ defineExpose({ :placeholder="placeholder" :class="ui.base({ class: props.ui?.base })" :disabled="disabled" - :required="decideRequired" + :required="required" :autocomplete="autocomplete" v-bind="{ ...$attrs, ...ariaAttrs }" @input="onInput" diff --git a/src/runtime/components/InputMenu.vue b/src/runtime/components/InputMenu.vue index 0c24a2954e..9a483bbe5e 100644 --- a/src/runtime/components/InputMenu.vue +++ b/src/runtime/components/InputMenu.vue @@ -212,14 +212,12 @@ const portalProps = usePortal(toRef(() => props.portal)) const contentProps = toRef(() => defu(props.content, { side: 'bottom', sideOffset: 8, collisionPadding: 8, position: 'popper' }) as ComboboxContentProps) const arrowProps = toRef(() => props.arrow as ComboboxArrowProps) -const { emitFormBlur, emitFormFocus, emitFormChange, emitFormInput, size: formGroupSize, color, id, name, highlight, disabled, ariaAttrs, required: formFieldRequired } = useFormField(props) +const { emitFormBlur, emitFormFocus, emitFormChange, emitFormInput, size: formGroupSize, color, id, name, highlight, disabled, ariaAttrs, required } = useFormField(props) const { orientation, size: buttonGroupSize } = useButtonGroup(props) const { isLeading, isTrailing, leadingIconName, trailingIconName } = useComponentIcons(toRef(() => defu(props, { trailingIcon: appConfig.ui.icons.chevronDown }))) const inputSize = computed(() => buttonGroupSize.value || formGroupSize.value) -const decideRequired = computed(() => props.required || formFieldRequired.value) - const [DefineCreateItemTemplate, ReuseCreateItemTemplate] = createReusableTemplate() const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.inputMenu || {}) })({ @@ -417,7 +415,7 @@ defineExpose({ v-slot="{ modelValue, open }" v-bind="rootProps" :name="name" - :required="decideRequired" + :required="required" :disabled="disabled" :class="ui.root({ class: [props.ui?.root, props.class] })" :as-child="!!multiple" diff --git a/src/runtime/components/InputNumber.vue b/src/runtime/components/InputNumber.vue index df302c67d4..7a34ef3398 100644 --- a/src/runtime/components/InputNumber.vue +++ b/src/runtime/components/InputNumber.vue @@ -100,12 +100,11 @@ const appConfig = useAppConfig() as InputNumber['AppConfig'] const rootProps = useForwardPropsEmits(reactivePick(props, 'as', 'modelValue', 'defaultValue', 'min', 'max', 'step', 'stepSnapping', 'formatOptions', 'disableWheelChange', 'invertWheelChange'), emits) -const { emitFormBlur, emitFormFocus, emitFormChange, emitFormInput, id, color, size: formGroupSize, name, highlight, disabled, ariaAttrs, required: formFieldRequired } = useFormField(props) +const { emitFormBlur, emitFormFocus, emitFormChange, emitFormInput, id, color, size: formGroupSize, name, highlight, disabled, ariaAttrs, required } = useFormField(props) const { orientation, size: buttonGroupSize } = useButtonGroup(props) const locale = computed(() => props.locale || codeLocale.value) const inputSize = computed(() => buttonGroupSize.value || formGroupSize.value) -const decideRequired = computed(() => props.required || formFieldRequired.value) const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.inputNumber || {}) })({ color: color.value, @@ -158,7 +157,7 @@ defineExpose({ :id="id" :class="ui.root({ class: [props.ui?.root, props.class] })" :name="name" - :required="decideRequired" + :required="required" :disabled="disabled" :locale="locale" @update:model-value="onUpdate" diff --git a/src/runtime/components/InputTags.vue b/src/runtime/components/InputTags.vue index 189f96eca5..0f01d9845f 100644 --- a/src/runtime/components/InputTags.vue +++ b/src/runtime/components/InputTags.vue @@ -88,12 +88,11 @@ const appConfig = useAppConfig() as InputTags['AppConfig'] const rootProps = useForwardPropsEmits(reactivePick(props, 'as', 'addOnPaste', 'addOnTab', 'addOnBlur', 'duplicate', 'delimiter', 'max', 'convertValue', 'displayValue'), emits) -const { emitFormBlur, emitFormFocus, emitFormChange, emitFormInput, size: formGroupSize, color, id, name, highlight, disabled, ariaAttrs, required: formFieldRequired } = useFormField(props) +const { emitFormBlur, emitFormFocus, emitFormChange, emitFormInput, size: formGroupSize, color, id, name, highlight, disabled, ariaAttrs, required } = useFormField(props) const { orientation, size: buttonGroupSize } = useButtonGroup(props) const { isLeading, isTrailing, leadingIconName, trailingIconName } = useComponentIcons(props) const inputSize = computed(() => buttonGroupSize.value || formGroupSize.value) -const decideRequired = computed(() => props.required || formFieldRequired.value) const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.inputTags || {}) })({ color: color.value, @@ -155,7 +154,7 @@ defineExpose({ :default-value="defaultValue" :class="ui.root({ class: [ui.base({ class: props.ui?.base }), props.ui?.root, props.class] })" v-bind="rootProps" - :required="decideRequired" + :required="required" :name="name" :disabled="disabled" @update:model-value="onUpdate" diff --git a/src/runtime/components/PinInput.vue b/src/runtime/components/PinInput.vue index c31ad6213d..99e446c120 100644 --- a/src/runtime/components/PinInput.vue +++ b/src/runtime/components/PinInput.vue @@ -67,9 +67,7 @@ const appConfig = useAppConfig() as PinInput['AppConfig'] const rootProps = useForwardPropsEmits(reactivePick(props, 'defaultValue', 'disabled', 'id', 'mask', 'modelValue', 'name', 'otp', 'type'), emits) -const { emitFormInput, emitFormFocus, emitFormChange, emitFormBlur, size, color, id, name, highlight, disabled, ariaAttrs, required: formFieldRequired } = useFormField(props) - -const decideRequired = computed(() => props.required || formFieldRequired.value) +const { emitFormInput, emitFormFocus, emitFormChange, emitFormBlur, size, color, id, name, highlight, disabled, ariaAttrs, required } = useFormField(props) const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.pinInput || {}) })({ color: color.value, @@ -117,7 +115,7 @@ defineExpose({ v-bind="{ ...rootProps, ...ariaAttrs }" :id="id" :name="name" - :required="decideRequired" + :required="required" :placeholder="placeholder" :class="ui.root({ class: [props.ui?.root, props.class] })" @update:model-value="emitFormInput()" diff --git a/src/runtime/components/RadioGroup.vue b/src/runtime/components/RadioGroup.vue index 8ca444e9ba..a15d27fd3f 100644 --- a/src/runtime/components/RadioGroup.vue +++ b/src/runtime/components/RadioGroup.vue @@ -101,7 +101,7 @@ const appConfig = useAppConfig() as RadioGroup['AppConfig'] const rootProps = useForwardPropsEmits(reactivePick(props, 'as', 'modelValue', 'defaultValue', 'orientation', 'loop'), emits) -const { emitFormChange, emitFormInput, color, name, size, id: _id, disabled, ariaAttrs, required: formFieldRequired } = useFormField>(props, { bind: false }) +const { emitFormChange, emitFormInput, color, name, size, id: _id, disabled, ariaAttrs, required } = useFormField>(props, { bind: false }) const id = _id.value ?? useId() const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.radioGroup || {}) })({ @@ -113,7 +113,6 @@ const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.radioGroup | variant: props.variant, indicator: props.indicator })) -const decideRequired = computed(() => props.required || formFieldRequired.value) function normalizeItem(item: any) { if (item === null) { @@ -167,7 +166,7 @@ function onUpdate(value: any) { :id="id" v-slot="{ modelValue }" v-bind="rootProps" - :required="decideRequired" + :required="required" :name="name" :disabled="disabled" diff --git a/src/runtime/components/Select.vue b/src/runtime/components/Select.vue index 9870b1b3f7..4d7236a4f4 100644 --- a/src/runtime/components/Select.vue +++ b/src/runtime/components/Select.vue @@ -169,10 +169,9 @@ const portalProps = usePortal(toRef(() => props.portal)) const contentProps = toRef(() => defu(props.content, { side: 'bottom', sideOffset: 8, collisionPadding: 8, position: 'popper' }) as SelectContentProps) const arrowProps = toRef(() => props.arrow as SelectArrowProps) -const { emitFormChange, emitFormInput, emitFormBlur, emitFormFocus, size: formGroupSize, color, id, name, highlight, disabled, ariaAttrs, required: formFieldRequired } = useFormField(props) +const { emitFormChange, emitFormInput, emitFormBlur, emitFormFocus, size: formGroupSize, color, id, name, highlight, disabled, ariaAttrs, required } = useFormField(props) const { orientation, size: buttonGroupSize } = useButtonGroup(props) const { isLeading, isTrailing, leadingIconName, trailingIconName } = useComponentIcons(toRef(() => defu(props, { trailingIcon: appConfig.ui.icons.chevronDown }))) -const decideRequired = computed(() => props.required || formFieldRequired.value) const selectSize = computed(() => buttonGroupSize.value || formGroupSize.value) @@ -258,7 +257,7 @@ defineExpose({ v-slot="{ modelValue, open }" :name="name" v-bind="rootProps" - :required="decideRequired" + :required="required" :autocomplete="autocomplete" :disabled="disabled" :default-value="(defaultValue as (AcceptableValue | AcceptableValue[]))" diff --git a/src/runtime/components/SelectMenu.vue b/src/runtime/components/SelectMenu.vue index f6d4e4dd37..80193341ab 100644 --- a/src/runtime/components/SelectMenu.vue +++ b/src/runtime/components/SelectMenu.vue @@ -209,12 +209,11 @@ const contentProps = toRef(() => defu(props.content, { side: 'bottom', sideOffse const arrowProps = toRef(() => props.arrow as ComboboxArrowProps) const searchInputProps = toRef(() => defu(props.searchInput, { placeholder: t('selectMenu.search'), variant: 'none' }) as InputProps) -const { emitFormBlur, emitFormFocus, emitFormInput, emitFormChange, size: formGroupSize, color, id, name, highlight, disabled, ariaAttrs, required: formFieldRequired } = useFormField(props) +const { emitFormBlur, emitFormFocus, emitFormInput, emitFormChange, size: formGroupSize, color, id, name, highlight, disabled, ariaAttrs, required } = useFormField(props) const { orientation, size: buttonGroupSize } = useButtonGroup(props) const { isLeading, isTrailing, leadingIconName, trailingIconName } = useComponentIcons(toRef(() => defu(props, { trailingIcon: appConfig.ui.icons.chevronDown }))) const selectSize = computed(() => buttonGroupSize.value || formGroupSize.value) -const decideRequired = computed(() => props.required || formFieldRequired.value) const [DefineCreateItemTemplate, ReuseCreateItemTemplate] = createReusableTemplate() @@ -400,7 +399,7 @@ defineExpose({ v-slot="{ modelValue, open }" v-bind="{ ...rootProps, ...$attrs, ...ariaAttrs }" ignore-filter - :required="decideRequired" + :required="required" as-child :name="name" :disabled="disabled" diff --git a/src/runtime/components/Slider.vue b/src/runtime/components/Slider.vue index e354427a52..b0be7c67e8 100644 --- a/src/runtime/components/Slider.vue +++ b/src/runtime/components/Slider.vue @@ -68,9 +68,8 @@ const appConfig = useAppConfig() as Slider['AppConfig'] const rootProps = useForwardPropsEmits(reactivePick(props, 'as', 'orientation', 'min', 'max', 'step', 'minStepsBetweenThumbs', 'inverted'), emits) -const { id, emitFormChange, emitFormInput, size, color, name, disabled, ariaAttrs, required: formFieldRequired } = useFormField(props) +const { id, emitFormChange, emitFormInput, size, color, name, disabled, ariaAttrs, required } = useFormField(props) -const decideRequired = computed(() => props.required || formFieldRequired.value) const defaultSliderValue = computed(() => { if (typeof props.defaultValue === 'number') { return [props.defaultValue] @@ -114,7 +113,7 @@ function onChange(value: any) { v-model="sliderValue" :name="name" :disabled="disabled" - :required="decideRequired" + :required="required" :class="ui.root({ class: [props.ui?.root, props.class] })" :default-value="defaultSliderValue" @update:model-value="emitFormInput()" diff --git a/src/runtime/components/Switch.vue b/src/runtime/components/Switch.vue index bc27838116..b8a25a1a16 100644 --- a/src/runtime/components/Switch.vue +++ b/src/runtime/components/Switch.vue @@ -75,7 +75,7 @@ const appConfig = useAppConfig() as Switch['AppConfig'] const rootProps = useForwardProps(reactivePick(props, 'value', 'defaultValue')) -const { id: _id, emitFormChange, emitFormInput, size, color, name, disabled, ariaAttrs, required: formFieldRequired } = useFormField(props) +const { id: _id, emitFormChange, emitFormInput, size, color, name, disabled, ariaAttrs, required } = useFormField(props) const id = _id.value ?? useId() const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.switch || {}) })({ @@ -85,7 +85,6 @@ const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.switch || {} loading: props.loading, disabled: disabled.value || props.loading })) -const decideRequired = computed(() => props.required || formFieldRequired.value) function onUpdate(value: any) { // @ts-expect-error - 'target' does not exist in type 'EventInit' @@ -103,7 +102,7 @@ function onUpdate(value: any) { :id="id" v-bind="{ ...rootProps, ...$attrs, ...ariaAttrs }" v-model="modelValue" - :required="decideRequired" + :required="required" :name="name" :disabled="disabled || loading" :class="ui.base({ class: props.ui?.base })" diff --git a/src/runtime/components/Textarea.vue b/src/runtime/components/Textarea.vue index 3349f86358..c3c9ce1d74 100644 --- a/src/runtime/components/Textarea.vue +++ b/src/runtime/components/Textarea.vue @@ -94,7 +94,7 @@ const modelValue = useVModel, 'modelValue', 'update:modelValue' const appConfig = useAppConfig() as Textarea['AppConfig'] -const { emitFormFocus, emitFormBlur, emitFormInput, emitFormChange, size, color, id, name, highlight, disabled, ariaAttrs, required: formFieldRequired } = useFormField>(props, { deferInputValidation: true }) +const { emitFormFocus, emitFormBlur, emitFormInput, emitFormChange, size, color, id, name, highlight, disabled, ariaAttrs, required } = useFormField>(props, { deferInputValidation: true }) const { isLeading, isTrailing, leadingIconName, trailingIconName } = useComponentIcons(props) const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.textarea || {}) })({ @@ -107,7 +107,6 @@ const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.textarea || leading: isLeading.value || !!props.avatar || !!slots.leading, trailing: isTrailing.value || !!slots.trailing })) -const decideRequired = computed(() => props.required || formFieldRequired.value) const textareaRef = ref(null) @@ -216,7 +215,7 @@ defineExpose({ :placeholder="placeholder" :class="ui.base({ class: props.ui?.base })" :disabled="disabled" - :required="decideRequired" + :required="required" v-bind="{ ...$attrs, ...ariaAttrs }" @input="onInput" @blur="onBlur" diff --git a/src/runtime/composables/useFormField.ts b/src/runtime/composables/useFormField.ts index 01458d633b..ff97a99d48 100644 --- a/src/runtime/composables/useFormField.ts +++ b/src/runtime/composables/useFormField.ts @@ -12,6 +12,7 @@ type Props = { size?: GetObjectField color?: GetObjectField highlight?: boolean + required?: boolean disabled?: boolean } @@ -77,7 +78,7 @@ export function useFormField(props?: Props, opts?: { bind?: boolean, defer size: computed(() => props?.size ?? formField?.value.size), color: computed(() => formField?.value.error ? 'error' : props?.color), highlight: computed(() => formField?.value.error ? true : props?.highlight), - required: computed(() => formField?.value.required), + required: computed(() => props?.required || formField?.value.required), disabled: computed(() => formOptions?.value.disabled || props?.disabled), emitFormBlur, emitFormInput, From 1759c524e0dd481c255b3a1f225706be27c8eb7c Mon Sep 17 00:00:00 2001 From: Benjamin Canac Date: Wed, 16 Jul 2025 22:05:34 +0200 Subject: [PATCH 03/16] up --- src/runtime/components/Checkbox.vue | 2 +- src/runtime/components/CheckboxGroup.vue | 3 +-- src/runtime/components/Input.vue | 2 +- src/runtime/components/InputMenu.vue | 2 +- src/runtime/components/InputNumber.vue | 2 +- src/runtime/components/InputTags.vue | 2 +- src/runtime/components/PinInput.vue | 2 +- src/runtime/components/RadioGroup.vue | 3 +-- src/runtime/components/Select.vue | 2 +- src/runtime/components/SelectMenu.vue | 2 +- src/runtime/components/Slider.vue | 2 +- src/runtime/components/Switch.vue | 2 +- src/runtime/components/Textarea.vue | 2 +- 13 files changed, 13 insertions(+), 15 deletions(-) diff --git a/src/runtime/components/Checkbox.vue b/src/runtime/components/Checkbox.vue index 59ba2d3837..0110399f59 100644 --- a/src/runtime/components/Checkbox.vue +++ b/src/runtime/components/Checkbox.vue @@ -78,7 +78,7 @@ const appConfig = useAppConfig() as Checkbox['AppConfig'] const rootProps = useForwardProps(reactivePick(props, 'value', 'defaultValue')) -const { id: _id, emitFormChange, emitFormInput, size, color, name, disabled, ariaAttrs, required } = useFormField(props) +const { emitFormChange, emitFormInput, color, name, size, id: _id, disabled, required, ariaAttrs } = useFormField(props) const id = _id.value ?? useId() const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.checkbox || {}) })({ diff --git a/src/runtime/components/CheckboxGroup.vue b/src/runtime/components/CheckboxGroup.vue index b1a5c1cafc..63bd3325bb 100644 --- a/src/runtime/components/CheckboxGroup.vue +++ b/src/runtime/components/CheckboxGroup.vue @@ -97,8 +97,7 @@ const rootProps = useForwardPropsEmits(reactivePick(props, 'as', 'modelValue', ' const checkboxProps = useForwardProps(reactivePick(props, 'variant', 'indicator', 'icon')) const proxySlots = omit(slots, ['legend']) -const { emitFormChange, emitFormInput, color, name, size, id: _id, disabled, ariaAttrs, required } = useFormField>(props, { bind: false }) - +const { emitFormChange, emitFormInput, color, name, size, id: _id, disabled, required, ariaAttrs } = useFormField>(props, { bind: false }) const id = _id.value ?? useId() const ui = computed(() => tv({ extend: theme, ...(appConfig.ui?.checkboxGroup || {}) })({ diff --git a/src/runtime/components/Input.vue b/src/runtime/components/Input.vue index 5820a73971..27e0259377 100644 --- a/src/runtime/components/Input.vue +++ b/src/runtime/components/Input.vue @@ -91,7 +91,7 @@ const modelValue = useVModel, 'modelValue', 'update:modelValue'>(p const appConfig = useAppConfig() as Input['AppConfig'] -const { emitFormBlur, emitFormInput, emitFormChange, size: formGroupSize, color, id, name, highlight, disabled, emitFormFocus, ariaAttrs, required } = useFormField>(props, { deferInputValidation: true }) +const { emitFormBlur, emitFormInput, emitFormChange, emitFormFocus, size: formGroupSize, color, id, name, highlight, disabled, required, ariaAttrs } = useFormField>(props, { deferInputValidation: true }) const { orientation, size: buttonGroupSize } = useButtonGroup>(props) const { isLeading, isTrailing, leadingIconName, trailingIconName } = useComponentIcons(props) diff --git a/src/runtime/components/InputMenu.vue b/src/runtime/components/InputMenu.vue index 88be95c2a4..59014e354f 100644 --- a/src/runtime/components/InputMenu.vue +++ b/src/runtime/components/InputMenu.vue @@ -212,7 +212,7 @@ const portalProps = usePortal(toRef(() => props.portal)) const contentProps = toRef(() => defu(props.content, { side: 'bottom', sideOffset: 8, collisionPadding: 8, position: 'popper' }) as ComboboxContentProps) const arrowProps = toRef(() => props.arrow as ComboboxArrowProps) -const { emitFormBlur, emitFormFocus, emitFormChange, emitFormInput, size: formGroupSize, color, id, name, highlight, disabled, ariaAttrs, required } = useFormField(props) +const { emitFormBlur, emitFormFocus, emitFormChange, emitFormInput, size: formGroupSize, color, id, name, highlight, disabled, required, ariaAttrs } = useFormField(props) const { orientation, size: buttonGroupSize } = useButtonGroup(props) const { isLeading, isTrailing, leadingIconName, trailingIconName } = useComponentIcons(toRef(() => defu(props, { trailingIcon: appConfig.ui.icons.chevronDown }))) diff --git a/src/runtime/components/InputNumber.vue b/src/runtime/components/InputNumber.vue index 7a34ef3398..44ae79f2cc 100644 --- a/src/runtime/components/InputNumber.vue +++ b/src/runtime/components/InputNumber.vue @@ -100,7 +100,7 @@ const appConfig = useAppConfig() as InputNumber['AppConfig'] const rootProps = useForwardPropsEmits(reactivePick(props, 'as', 'modelValue', 'defaultValue', 'min', 'max', 'step', 'stepSnapping', 'formatOptions', 'disableWheelChange', 'invertWheelChange'), emits) -const { emitFormBlur, emitFormFocus, emitFormChange, emitFormInput, id, color, size: formGroupSize, name, highlight, disabled, ariaAttrs, required } = useFormField(props) +const { emitFormBlur, emitFormFocus, emitFormChange, emitFormInput, id, color, size: formGroupSize, name, highlight, disabled, required, ariaAttrs } = useFormField(props) const { orientation, size: buttonGroupSize } = useButtonGroup(props) const locale = computed(() => props.locale || codeLocale.value) diff --git a/src/runtime/components/InputTags.vue b/src/runtime/components/InputTags.vue index 0f01d9845f..32bdb27f77 100644 --- a/src/runtime/components/InputTags.vue +++ b/src/runtime/components/InputTags.vue @@ -88,7 +88,7 @@ const appConfig = useAppConfig() as InputTags['AppConfig'] const rootProps = useForwardPropsEmits(reactivePick(props, 'as', 'addOnPaste', 'addOnTab', 'addOnBlur', 'duplicate', 'delimiter', 'max', 'convertValue', 'displayValue'), emits) -const { emitFormBlur, emitFormFocus, emitFormChange, emitFormInput, size: formGroupSize, color, id, name, highlight, disabled, ariaAttrs, required } = useFormField(props) +const { emitFormBlur, emitFormFocus, emitFormChange, emitFormInput, size: formGroupSize, color, id, name, highlight, disabled, required, ariaAttrs } = useFormField(props) const { orientation, size: buttonGroupSize } = useButtonGroup(props) const { isLeading, isTrailing, leadingIconName, trailingIconName } = useComponentIcons(props) diff --git a/src/runtime/components/PinInput.vue b/src/runtime/components/PinInput.vue index 99e446c120..14ebca4329 100644 --- a/src/runtime/components/PinInput.vue +++ b/src/runtime/components/PinInput.vue @@ -67,7 +67,7 @@ const appConfig = useAppConfig() as PinInput['AppConfig'] const rootProps = useForwardPropsEmits(reactivePick(props, 'defaultValue', 'disabled', 'id', 'mask', 'modelValue', 'name', 'otp', 'type'), emits) -const { emitFormInput, emitFormFocus, emitFormChange, emitFormBlur, size, color, id, name, highlight, disabled, ariaAttrs, required } = useFormField(props) +const { emitFormInput, emitFormFocus, emitFormChange, emitFormBlur, size, color, id, name, highlight, disabled, required, ariaAttrs } = useFormField(props) const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.pinInput || {}) })({ color: color.value, diff --git a/src/runtime/components/RadioGroup.vue b/src/runtime/components/RadioGroup.vue index e6e5078c39..1e1fb566e8 100644 --- a/src/runtime/components/RadioGroup.vue +++ b/src/runtime/components/RadioGroup.vue @@ -103,7 +103,7 @@ const appConfig = useAppConfig() as RadioGroup['AppConfig'] const rootProps = useForwardPropsEmits(reactivePick(props, 'as', 'modelValue', 'defaultValue', 'orientation', 'loop'), emits) -const { emitFormChange, emitFormInput, color, name, size, id: _id, disabled, ariaAttrs, required } = useFormField>(props, { bind: false }) +const { emitFormChange, emitFormInput, color, name, size, id: _id, disabled, required, ariaAttrs } = useFormField>(props, { bind: false }) const id = _id.value ?? useId() const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.radioGroup || {}) })({ @@ -169,7 +169,6 @@ function onUpdate(value: any) { v-slot="{ modelValue }" v-bind="rootProps" :required="required" - :name="name" :disabled="disabled" :class="ui.root({ class: [props.ui?.root, props.class] })" diff --git a/src/runtime/components/Select.vue b/src/runtime/components/Select.vue index 4d7236a4f4..64f66f5bad 100644 --- a/src/runtime/components/Select.vue +++ b/src/runtime/components/Select.vue @@ -169,7 +169,7 @@ const portalProps = usePortal(toRef(() => props.portal)) const contentProps = toRef(() => defu(props.content, { side: 'bottom', sideOffset: 8, collisionPadding: 8, position: 'popper' }) as SelectContentProps) const arrowProps = toRef(() => props.arrow as SelectArrowProps) -const { emitFormChange, emitFormInput, emitFormBlur, emitFormFocus, size: formGroupSize, color, id, name, highlight, disabled, ariaAttrs, required } = useFormField(props) +const { emitFormChange, emitFormInput, emitFormBlur, emitFormFocus, size: formGroupSize, color, id, name, highlight, disabled, required, ariaAttrs } = useFormField(props) const { orientation, size: buttonGroupSize } = useButtonGroup(props) const { isLeading, isTrailing, leadingIconName, trailingIconName } = useComponentIcons(toRef(() => defu(props, { trailingIcon: appConfig.ui.icons.chevronDown }))) diff --git a/src/runtime/components/SelectMenu.vue b/src/runtime/components/SelectMenu.vue index a24825598d..ccdffa635b 100644 --- a/src/runtime/components/SelectMenu.vue +++ b/src/runtime/components/SelectMenu.vue @@ -209,7 +209,7 @@ const contentProps = toRef(() => defu(props.content, { side: 'bottom', sideOffse const arrowProps = toRef(() => props.arrow as ComboboxArrowProps) const searchInputProps = toRef(() => defu(props.searchInput, { placeholder: t('selectMenu.search'), variant: 'none' }) as InputProps) -const { emitFormBlur, emitFormFocus, emitFormInput, emitFormChange, size: formGroupSize, color, id, name, highlight, disabled, ariaAttrs, required } = useFormField(props) +const { emitFormBlur, emitFormFocus, emitFormInput, emitFormChange, size: formGroupSize, color, id, name, highlight, disabled, required, ariaAttrs } = useFormField(props) const { orientation, size: buttonGroupSize } = useButtonGroup(props) const { isLeading, isTrailing, leadingIconName, trailingIconName } = useComponentIcons(toRef(() => defu(props, { trailingIcon: appConfig.ui.icons.chevronDown }))) diff --git a/src/runtime/components/Slider.vue b/src/runtime/components/Slider.vue index b0be7c67e8..1e13af1ab9 100644 --- a/src/runtime/components/Slider.vue +++ b/src/runtime/components/Slider.vue @@ -68,7 +68,7 @@ const appConfig = useAppConfig() as Slider['AppConfig'] const rootProps = useForwardPropsEmits(reactivePick(props, 'as', 'orientation', 'min', 'max', 'step', 'minStepsBetweenThumbs', 'inverted'), emits) -const { id, emitFormChange, emitFormInput, size, color, name, disabled, ariaAttrs, required } = useFormField(props) +const { id, emitFormChange, emitFormInput, size, color, name, disabled, required, ariaAttrs } = useFormField(props) const defaultSliderValue = computed(() => { if (typeof props.defaultValue === 'number') { diff --git a/src/runtime/components/Switch.vue b/src/runtime/components/Switch.vue index b8a25a1a16..dff21ffb2a 100644 --- a/src/runtime/components/Switch.vue +++ b/src/runtime/components/Switch.vue @@ -75,7 +75,7 @@ const appConfig = useAppConfig() as Switch['AppConfig'] const rootProps = useForwardProps(reactivePick(props, 'value', 'defaultValue')) -const { id: _id, emitFormChange, emitFormInput, size, color, name, disabled, ariaAttrs, required } = useFormField(props) +const { emitFormChange, emitFormInput, color, name, size, id: _id, disabled, required, ariaAttrs } = useFormField(props) const id = _id.value ?? useId() const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.switch || {}) })({ diff --git a/src/runtime/components/Textarea.vue b/src/runtime/components/Textarea.vue index c3c9ce1d74..9f2d21e586 100644 --- a/src/runtime/components/Textarea.vue +++ b/src/runtime/components/Textarea.vue @@ -94,7 +94,7 @@ const modelValue = useVModel, 'modelValue', 'update:modelValue' const appConfig = useAppConfig() as Textarea['AppConfig'] -const { emitFormFocus, emitFormBlur, emitFormInput, emitFormChange, size, color, id, name, highlight, disabled, ariaAttrs, required } = useFormField>(props, { deferInputValidation: true }) +const { emitFormFocus, emitFormBlur, emitFormInput, emitFormChange, size, color, id, name, highlight, disabled, required, ariaAttrs } = useFormField>(props, { deferInputValidation: true }) const { isLeading, isTrailing, leadingIconName, trailingIconName } = useComponentIcons(props) const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.textarea || {}) })({ From de43537f1d89299332520ef9d75c3b8f17e09c06 Mon Sep 17 00:00:00 2001 From: Benjamin Canac Date: Wed, 16 Jul 2025 22:38:21 +0200 Subject: [PATCH 04/16] up --- .../__snapshots__/Checkbox-vue.spec.ts.snap | 58 ++--- .../__snapshots__/Checkbox.spec.ts.snap | 58 ++--- .../CheckboxGroup-vue.spec.ts.snap | 198 +++++++++--------- .../__snapshots__/CheckboxGroup.spec.ts.snap | 198 +++++++++--------- .../__snapshots__/Select-vue.spec.ts.snap | 102 ++++----- .../__snapshots__/Select.spec.ts.snap | 102 ++++----- .../__snapshots__/Switch-vue.spec.ts.snap | 48 ++--- .../__snapshots__/Switch.spec.ts.snap | 48 ++--- .../__snapshots__/Table-vue.spec.ts.snap | 28 +-- .../__snapshots__/Table.spec.ts.snap | 28 +-- 10 files changed, 434 insertions(+), 434 deletions(-) diff --git a/test/components/__snapshots__/Checkbox-vue.spec.ts.snap b/test/components/__snapshots__/Checkbox-vue.spec.ts.snap index 09ab38f729..b171856622 100644 --- a/test/components/__snapshots__/Checkbox-vue.spec.ts.snap +++ b/test/components/__snapshots__/Checkbox-vue.spec.ts.snap @@ -2,7 +2,7 @@ exports[`Checkbox > renders with ariaLabel correctly 1`] = ` "
-
@@ -12,7 +12,7 @@ exports[`Checkbox > renders with ariaLabel correctly 1`] = ` exports[`Checkbox > renders with as correctly 1`] = ` "
-
@@ -22,7 +22,7 @@ exports[`Checkbox > renders with as correctly 1`] = ` exports[`Checkbox > renders with class correctly 1`] = ` "
-
@@ -32,7 +32,7 @@ exports[`Checkbox > renders with class correctly 1`] = ` exports[`Checkbox > renders with defaultValue correctly 1`] = ` "
-
@@ -41,7 +41,7 @@ exports[`Checkbox > renders with defaultValue correctly 1`] = ` exports[`Checkbox > renders with description correctly 1`] = ` "
-
@@ -53,7 +53,7 @@ exports[`Checkbox > renders with description correctly 1`] = ` exports[`Checkbox > renders with description slot correctly 1`] = ` "
-
@@ -65,7 +65,7 @@ exports[`Checkbox > renders with description slot correctly 1`] = ` exports[`Checkbox > renders with disabled correctly 1`] = ` "
-
@@ -75,7 +75,7 @@ exports[`Checkbox > renders with disabled correctly 1`] = ` exports[`Checkbox > renders with icon correctly 1`] = ` "
-
@@ -85,7 +85,7 @@ exports[`Checkbox > renders with icon correctly 1`] = ` exports[`Checkbox > renders with id correctly 1`] = ` "
-
@@ -95,7 +95,7 @@ exports[`Checkbox > renders with id correctly 1`] = ` exports[`Checkbox > renders with indeterminate correctly 1`] = ` "
-
@@ -104,7 +104,7 @@ exports[`Checkbox > renders with indeterminate correctly 1`] = ` exports[`Checkbox > renders with indeterminateIcon correctly 1`] = ` "
-
@@ -113,7 +113,7 @@ exports[`Checkbox > renders with indeterminateIcon correctly 1`] = ` exports[`Checkbox > renders with indicator end correctly 1`] = ` "
-
@@ -123,7 +123,7 @@ exports[`Checkbox > renders with indicator end correctly 1`] = ` exports[`Checkbox > renders with indicator hidden correctly 1`] = ` "
-
@@ -133,7 +133,7 @@ exports[`Checkbox > renders with indicator hidden correctly 1`] = ` exports[`Checkbox > renders with indicator start correctly 1`] = ` "
-
@@ -143,7 +143,7 @@ exports[`Checkbox > renders with indicator start correctly 1`] = ` exports[`Checkbox > renders with label correctly 1`] = ` "
-
@@ -155,7 +155,7 @@ exports[`Checkbox > renders with label correctly 1`] = ` exports[`Checkbox > renders with label slot correctly 1`] = ` "
-
@@ -167,7 +167,7 @@ exports[`Checkbox > renders with label slot correctly 1`] = ` exports[`Checkbox > renders with modelValue correctly 1`] = ` "
-
@@ -176,7 +176,7 @@ exports[`Checkbox > renders with modelValue correctly 1`] = ` exports[`Checkbox > renders with name correctly 1`] = ` "
-
@@ -186,7 +186,7 @@ exports[`Checkbox > renders with name correctly 1`] = ` exports[`Checkbox > renders with neutral variant card correctly 1`] = ` "