Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
73 commits
Select commit Hold shift + click to select a range
f9aaa99
feat(FormField): add required field support across form components
rdjanuar Jul 16, 2025
448b264
Merge branch 'v3' into fix/issues-4529
rdjanuar Jul 16, 2025
6889618
fix(form): simplify required field logic by moving it to useFormField
rdjanuar Jul 16, 2025
52e4dad
Merge branch 'v3' into fix/issues-4529
rdjanuar Jul 16, 2025
9846225
Merge branch 'v3' into pr/4534
benjamincanac Jul 16, 2025
1759c52
up
benjamincanac Jul 16, 2025
de43537
up
benjamincanac Jul 16, 2025
52303fe
Merge branch 'v3' into fix/issues-4529
rdjanuar Jul 17, 2025
a79bb6f
test(FormField): add required binding test
rdjanuar Jul 19, 2025
ef955d4
refactor(FormField): improve type safety for required fields
rdjanuar Jul 19, 2025
0b09606
up
rdjanuar Jul 19, 2025
d6914fb
up
rdjanuar Jul 19, 2025
e5ab984
up
rdjanuar Jul 19, 2025
27ad4b8
up
rdjanuar Jul 19, 2025
cacb095
fix(FormField): revert FieldProps
rdjanuar Jul 23, 2025
8519389
fix(docs): revert form component example
rdjanuar Jul 24, 2025
9cd9960
Merge branch 'v3' into fix/issues-4529
rdjanuar Jul 24, 2025
dc84947
Merge branch 'v3' into fix/issues-4529
rdjanuar Jul 28, 2025
778e62b
up
rdjanuar Jul 28, 2025
885bb1d
test(FormField): fix missing import
rdjanuar Jul 31, 2025
38ff468
test(FormField): update test to handle FileUpload component
rdjanuar Jul 31, 2025
720e2e2
Merge branch 'v3' into fix/issues-4529
rdjanuar Aug 1, 2025
f9884cb
Merge branch 'v3' into fix/issues-4529
rdjanuar Aug 9, 2025
eed4511
fix: lint
rdjanuar Aug 9, 2025
fdcd0fa
update: snapshot
rdjanuar Aug 9, 2025
5a13184
test: update snapshot
rdjanuar Aug 9, 2025
0090f58
Merge branch 'v3' into pr/4534
benjamincanac Aug 14, 2025
8a44e29
test: update snapshots
benjamincanac Aug 14, 2025
87c0f11
up
benjamincanac Aug 14, 2025
dd10323
up
benjamincanac Aug 14, 2025
16b39b6
chore(readme): add link to v4 branch
benjamincanac Sep 1, 2025
6ccc060
chore(deps): update @nuxt/ui-pro
benjamincanac Sep 1, 2025
901bf7c
chore(github): remove `documentation` label from pr labeler workflow
benjamincanac Sep 1, 2025
3bff833
docs(templates): use `nuxt-ui-templates` org
benjamincanac Sep 2, 2025
0da1854
docs: update figma links
benjamincanac Sep 2, 2025
fc915a5
docs: update social links
benjamincanac Sep 2, 2025
730bc2b
chore(vercel): remove deprecated flag
benjamincanac Sep 2, 2025
b0cf86d
docs: prepare vercel deployment
benjamincanac Sep 2, 2025
05a7b5d
docs: enable vercel analytics
benjamincanac Sep 2, 2025
25f93a3
chore(scripts): add ignore build step for vercel
benjamincanac Sep 2, 2025
819b7dd
docs(templates): clean urls
benjamincanac Sep 2, 2025
3eb8b2b
docs(app): hide banner
benjamincanac Sep 5, 2025
81ffc74
chore(deps): update actions/stale action to v10 (v3) (#4911)
renovate[bot] Sep 8, 2025
e69c21c
chore(deps): update actions/labeler action to v6 (v3) (#4909)
renovate[bot] Sep 8, 2025
16c22ca
chore(deps): update actions/setup-node action to v5 (v3) (#4910)
renovate[bot] Sep 8, 2025
51719d2
chore(deps): update tailwindcss to ^4.1.13 (v3) (#4908)
renovate[bot] Sep 8, 2025
bdb9f44
chore(deps): update all non-major dependencies (v3) (#4861)
renovate[bot] Sep 8, 2025
0996e3b
chore(deps): move pnpm config in workspace
benjamincanac Sep 8, 2025
e38f8d9
docs(avatar/avatar-group): add mask example (#4897)
maximepvrt Sep 8, 2025
5b9d9d8
fix(InputMenu/Select/SelectMenu): show falsy value when model value i…
rdjanuar Sep 8, 2025
91f86d9
fix(FileUpload): add missing `button` type
benjamincanac Sep 9, 2025
0a8ead7
fix(Form): handling race condition on `clear` function (#4843)
rdjanuar Sep 12, 2025
e5cb55b
fix(locale): improve `id` name (#4890)
reinacchi Sep 5, 2025
c39c34c
chore(deps): update all non-major dependencies (v3) (#4920)
renovate[bot] Sep 15, 2025
3060e3d
chore(deps): update vue-tsc to ^3.0.7 (v3) (#4980)
renovate[bot] Sep 15, 2025
082f9ad
chore(deps): update dependency capture-website to v5 (v3) (#4983)
renovate[bot] Sep 15, 2025
53bc5a6
docs(team): update grid
benjamincanac Sep 15, 2025
296feac
chore(release): v3.3.4
benjamincanac Sep 15, 2025
7da16e2
chore(deps): update dependency @nuxt/ui-pro to ^3.3.4 (v3) (#4989)
renovate[bot] Sep 15, 2025
526cb81
fix(types): resolve ambient declaration error in `icons` type (#4991)
vitpetricak Sep 16, 2025
1e95408
chore(deps): add `wrangler` for playground
benjamincanac Sep 16, 2025
dc28907
chore(deps): bump vue-sfc-transformer + mkdist
benjamincanac Sep 16, 2025
5456b8c
fix(Progress): improve `status-position` when 0 (#4994)
HugoRCD Sep 16, 2025
7ef124c
Merge branch 'v3' into fix/issues-4529
rdjanuar Sep 17, 2025
f33718f
fix: conflict
rdjanuar Oct 13, 2025
cbe27f0
up
rdjanuar Oct 13, 2025
a0a5a28
up
rdjanuar Oct 13, 2025
a2b6a77
up
rdjanuar Oct 13, 2025
b016e7a
fix: conflict
rdjanuar Oct 13, 2025
e8d871f
up
rdjanuar Oct 13, 2025
30f0d0d
up
rdjanuar Oct 13, 2025
a34192b
up
rdjanuar Oct 13, 2025
d56466e
up
rdjanuar Oct 13, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -233,4 +233,4 @@
"framework",
"ui-framework"
]
}
}
2 changes: 1 addition & 1 deletion pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions src/runtime/components/Checkbox.vue
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,9 @@ const modelValue = defineModel<boolean | 'indeterminate'>({ 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<CheckboxProps>(props)
const { emitFormChange, emitFormInput, color, name, size, id: _id, disabled, required, ariaAttrs } = useFormField<CheckboxProps>(props)
const id = _id.value ?? useId()

const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.checkbox || {}) })({
Expand Down Expand Up @@ -109,6 +109,7 @@ function onUpdate(value: any) {
v-bind="{ ...rootProps, ...$attrs, ...ariaAttrs }"
v-model="modelValue"
:name="name"
:required="required"
:disabled="disabled"
:class="ui.base({ class: props.ui?.base })"
@update:model-value="onUpdate"
Expand Down
5 changes: 3 additions & 2 deletions src/runtime/components/CheckboxGroup.vue
Original file line number Diff line number Diff line change
Expand Up @@ -97,11 +97,11 @@ const slots = defineSlots<CheckboxGroupSlots<T>>()

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 getProxySlots = () => omit(slots, ['legend'])

const { emitFormChange, emitFormInput, color, name, size, id: _id, disabled, ariaAttrs } = useFormField<CheckboxGroupProps<T>>(props, { bind: false })
const { emitFormChange, emitFormInput, color, name, size, id: _id, disabled, required, ariaAttrs } = useFormField<CheckboxGroupProps<T>>(props, { bind: false })
const id = _id.value ?? useId()

const ui = computed(() => tv({ extend: theme, ...(appConfig.ui?.checkboxGroup || {}) })({
Expand Down Expand Up @@ -163,6 +163,7 @@ function onUpdate(value: any) {
<CheckboxGroupRoot
:id="id"
v-bind="rootProps"
:required="required"
:name="name"
:disabled="disabled"
:class="ui.root({ class: [props.ui?.root, props.class] })"
Expand Down
2 changes: 1 addition & 1 deletion src/runtime/components/FileUpload.vue
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ const { isDragging, open, inputRef, dropzoneRef } = useFileUpload({
dropzone: props.dropzone,
onUpdate
})
const { emitFormInput, emitFormChange, id, name, disabled, ariaAttrs } = useFormField<FileUploadProps>(props)
const { emitFormInput, emitFormChange, id, name, disabled, required, ariaAttrs } = useFormField<FileUploadProps<M>>(props)

const variant = computed(() => props.multiple ? 'area' : props.variant)
const layout = computed(() => props.variant === 'button' && !props.multiple ? 'grid' : props.layout)
Expand Down
1 change: 1 addition & 0 deletions src/runtime/components/FormField.vue
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ provide(formFieldInjectionKey, computed(() => ({
name: props.name,
size: props.size,
eagerValidation: props.eagerValidation,
required: props.required,
validateOnInputDelay: props.validateOnInputDelay,
errorPattern: props.errorPattern,
hint: props.hint,
Expand Down
2 changes: 1 addition & 1 deletion src/runtime/components/Input.vue
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ const modelValue = useVModel<InputProps<T>, 'modelValue', 'update:modelValue'>(p

const appConfig = useAppConfig() as Input['AppConfig']

const { emitFormBlur, emitFormInput, emitFormChange, size: formGroupSize, color, id, name, highlight, disabled, emitFormFocus, ariaAttrs } = useFormField<InputProps<T>>(props, { deferInputValidation: true })
const { emitFormBlur, emitFormInput, emitFormChange, size: formGroupSize, color, id, name, highlight, disabled, emitFormFocus, ariaAttrs, required } = useFormField<InputProps<T>>(props, { deferInputValidation: true })
const { orientation, size: fieldGroupSize } = useFieldGroup<InputProps<T>>(props)
const { isLeading, isTrailing, leadingIconName, trailingIconName } = useComponentIcons(props)

Expand Down
5 changes: 3 additions & 2 deletions src/runtime/components/InputMenu.vue
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ 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', 'openOnClick', 'openOnFocus'), emits)
const rootProps = useForwardPropsEmits(reactivePick(props, 'as', 'modelValue', 'defaultValue', 'open', 'defaultOpen', 'multiple', 'resetSearchTermOnBlur', 'resetSearchTermOnSelect', 'highlightOnHover', 'openOnClick', 'openOnFocus'), 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)
Expand All @@ -241,7 +241,7 @@ const virtualizerProps = toRef(() => !!props.virtualize && defu(typeof props.vir
})[props.size || 'md']
}))

const { emitFormBlur, emitFormFocus, emitFormChange, emitFormInput, size: formGroupSize, color, id, name, highlight, disabled, ariaAttrs } = useFormField<InputProps>(props)
const { emitFormBlur, emitFormFocus, emitFormChange, emitFormInput, size: formGroupSize, color, id, name, highlight, disabled, ariaAttrs, required } = useFormField<InputProps>(props)
const { orientation, size: fieldGroupSize } = useFieldGroup<InputProps>(props)
const { isLeading, isTrailing, leadingIconName, trailingIconName } = useComponentIcons(toRef(() => defu(props, { trailingIcon: appConfig.ui.icons.chevronDown })))

Expand Down Expand Up @@ -503,6 +503,7 @@ defineExpose({
v-slot="{ modelValue, open }"
v-bind="rootProps"
:name="name"
:required="required"
:disabled="disabled"
:class="ui.root({ class: [props.ui?.root, props.class] })"
:as-child="!!multiple"
Expand Down
3 changes: 2 additions & 1 deletion src/runtime/components/InputNumber.vue
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ const appConfig = useAppConfig() as InputNumber['AppConfig']

const rootProps = useForwardPropsEmits(reactivePick(props, 'as', 'defaultValue', 'min', 'max', 'step', 'stepSnapping', 'formatOptions', 'disableWheelChange', 'invertWheelChange', 'readonly'), emits)

const { emitFormBlur, emitFormFocus, emitFormChange, emitFormInput, id, color, size: formGroupSize, name, highlight, disabled, ariaAttrs } = useFormField<InputNumberProps>(props)
const { emitFormBlur, emitFormFocus, emitFormChange, emitFormInput, id, color, size: formGroupSize, name, highlight, disabled, ariaAttrs, required } = useFormField<InputNumberProps>(props)
const { orientation, size: fieldGroupSize } = useFieldGroup<InputNumberProps>(props)

const locale = computed(() => props.locale || codeLocale.value)
Expand Down Expand Up @@ -166,6 +166,7 @@ defineExpose({
:model-value="modelValue"
:class="ui.root({ class: [props.ui?.root, props.class] })"
:name="name"
:required="required"
:disabled="disabled"
:locale="locale"
@update:model-value="onUpdate"
Expand Down
5 changes: 3 additions & 2 deletions src/runtime/components/InputTags.vue
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,9 @@ const slots = defineSlots<InputTagsSlots<T>>()

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<InputTagsProps>(props)
const { emitFormBlur, emitFormFocus, emitFormChange, emitFormInput, size: formGroupSize, color, id, name, highlight, disabled, ariaAttrs, required } = useFormField<InputTagsProps>(props)
const { orientation, size: fieldGroupSize } = useFieldGroup<InputTagsProps>(props)
const { isLeading, isTrailing, leadingIconName, trailingIconName } = useComponentIcons(props)

Expand Down Expand Up @@ -154,6 +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="required"
:name="name"
:disabled="disabled"
@update:model-value="onUpdate"
Expand Down
5 changes: 3 additions & 2 deletions src/runtime/components/PinInput.vue
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,9 @@ const emits = defineEmits<PinInputEmits<T>>()

const appConfig = useAppConfig() as PinInput['AppConfig']

const rootProps = useForwardPropsEmits(reactivePick(props, 'disabled', 'id', 'mask', 'name', 'otp', 'required', 'type'), emits)
const rootProps = useForwardPropsEmits(reactivePick(props, 'id', 'mask', 'name', 'otp', 'required', 'type'), emits)

const { emitFormInput, emitFormFocus, emitFormChange, emitFormBlur, size, color, id, name, highlight, disabled, ariaAttrs } = useFormField<PinInputProps>(props)
const { emitFormInput, emitFormFocus, emitFormChange, emitFormBlur, size, color, id, name, highlight, disabled, required, ariaAttrs } = useFormField<PinInputProps>(props)

const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.pinInput || {}) })({
color: color.value,
Expand Down Expand Up @@ -116,6 +116,7 @@ defineExpose({
v-bind="{ ...rootProps, ...ariaAttrs }"
:id="id"
:name="name"
:required="required"
:placeholder="placeholder"
:model-value="(modelValue as PinInputValue<T>)"
:default-value="(defaultValue as PinInputValue<T>[])"
Expand Down
5 changes: 3 additions & 2 deletions src/runtime/components/RadioGroup.vue
Original file line number Diff line number Diff line change
Expand Up @@ -106,9 +106,9 @@ const slots = defineSlots<RadioGroupSlots<T>>()

const appConfig = useAppConfig() as RadioGroup['AppConfig']

const rootProps = useForwardPropsEmits(reactivePick(props, 'as', 'loop', 'required'), emits)
const rootProps = useForwardPropsEmits(reactivePick(props, 'as', 'loop'), emits)

const { emitFormChange, emitFormInput, color, name, size, id: _id, disabled, ariaAttrs } = useFormField<RadioGroupProps<T>>(props, { bind: false })
const { emitFormChange, emitFormInput, color, name, size, id: _id, disabled, required, ariaAttrs } = useFormField<RadioGroupProps<T>>(props, { bind: false })
const id = _id.value ?? useId()

const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.radioGroup || {}) })({
Expand Down Expand Up @@ -175,6 +175,7 @@ function onUpdate(value: any) {
:model-value="(modelValue as Exclude<RadioGroupItem, boolean> | Exclude<RadioGroupItem, boolean>[])"
:default-value="(defaultValue as Exclude<RadioGroupItem, boolean> | Exclude<RadioGroupItem, boolean>[])"
:orientation="orientation"
:required="required"
:name="name"
:disabled="disabled"
:class="ui.root({ class: [props.ui?.root, props.class] })"
Expand Down
5 changes: 3 additions & 2 deletions src/runtime/components/Select.vue
Original file line number Diff line number Diff line change
Expand Up @@ -165,12 +165,12 @@ const slots = defineSlots<SelectSlots<T, VK, M>>()

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<InputProps>(props)
const { emitFormChange, emitFormInput, emitFormBlur, emitFormFocus, size: formGroupSize, color, id, name, highlight, disabled, ariaAttrs, required } = useFormField<InputProps>(props)
const { orientation, size: fieldGroupSize } = useFieldGroup<InputProps>(props)
const { isLeading, isTrailing, leadingIconName, trailingIconName } = useComponentIcons(toRef(() => defu(props, { trailingIcon: appConfig.ui.icons.chevronDown })))

Expand Down Expand Up @@ -266,6 +266,7 @@ defineExpose({
v-slot="{ modelValue, open }"
:name="name"
v-bind="rootProps"
:required="required"
:autocomplete="autocomplete"
:disabled="disabled"
:default-value="(defaultValue as Exclude<SelectItem, boolean> | Exclude<SelectItem, boolean>[])"
Expand Down
5 changes: 3 additions & 2 deletions src/runtime/components/SelectMenu.vue
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ 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)
Expand All @@ -237,7 +237,7 @@ const virtualizerProps = toRef(() => !!props.virtualize && defu(typeof props.vir
}))
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<InputProps>(props)
const { emitFormBlur, emitFormFocus, emitFormInput, emitFormChange, size: formGroupSize, color, id, name, highlight, disabled, ariaAttrs, required } = useFormField<InputProps>(props)
const { orientation, size: fieldGroupSize } = useFieldGroup<InputProps>(props)
const { isLeading, isTrailing, leadingIconName, trailingIconName } = useComponentIcons(toRef(() => defu(props, { trailingIcon: appConfig.ui.icons.chevronDown })))

Expand Down Expand Up @@ -487,6 +487,7 @@ defineExpose({
v-slot="{ modelValue, open }"
v-bind="{ ...rootProps, ...$attrs, ...ariaAttrs }"
ignore-filter
:required="required"
as-child
:name="name"
:disabled="disabled"
Expand Down
4 changes: 3 additions & 1 deletion src/runtime/components/Slider.vue
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export interface SliderProps extends Pick<SliderRootProps, 'name' | 'disabled' |
tooltip?: boolean | TooltipProps
/** The value of the slider when initially rendered. Use when you do not need to control the state of the slider. */
defaultValue?: number | number[]
required?: boolean
class?: any
ui?: Slider['slots']
}
Expand Down Expand Up @@ -66,7 +67,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 } = useFormField<SliderProps>(props)
const { id, emitFormChange, emitFormInput, size, color, name, disabled, required, ariaAttrs } = useFormField<SliderProps>(props)

const defaultSliderValue = computed(() => {
if (typeof props.defaultValue === 'number') {
Expand Down Expand Up @@ -111,6 +112,7 @@ function onChange(value: any) {
v-model="sliderValue"
:name="name"
:disabled="disabled"
:required="required"
:class="ui.root({ class: [props.ui?.root, props.class] })"
:default-value="defaultSliderValue"
@update:model-value="emitFormInput()"
Expand Down
5 changes: 3 additions & 2 deletions src/runtime/components/Switch.vue
Original file line number Diff line number Diff line change
Expand Up @@ -74,9 +74,9 @@ const modelValue = defineModel<boolean>({ 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<SwitchProps>(props)
const { emitFormChange, emitFormInput, color, name, size, id: _id, disabled, required, ariaAttrs } = useFormField<SwitchProps>(props)
const id = _id.value ?? useId()

const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.switch || {}) })({
Expand All @@ -103,6 +103,7 @@ function onUpdate(value: any) {
:id="id"
v-bind="{ ...rootProps, ...$attrs, ...ariaAttrs }"
v-model="modelValue"
:required="required"
:name="name"
:disabled="disabled || loading"
:class="ui.base({ class: props.ui?.base })"
Expand Down
2 changes: 1 addition & 1 deletion src/runtime/components/Textarea.vue
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ const modelValue = useVModel<TextareaProps<T>, 'modelValue', 'update:modelValue'

const appConfig = useAppConfig() as Textarea['AppConfig']

const { emitFormFocus, emitFormBlur, emitFormInput, emitFormChange, size, color, id, name, highlight, disabled, ariaAttrs } = useFormField<TextareaProps<T>>(props, { deferInputValidation: true })
const { emitFormFocus, emitFormBlur, emitFormInput, emitFormChange, size, color, id, name, highlight, disabled, required, ariaAttrs } = useFormField<TextareaProps<T>>(props, { deferInputValidation: true })
const { isLeading, isTrailing, leadingIconName, trailingIconName } = useComponentIcons(props)

const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.textarea || {}) })({
Expand Down
2 changes: 2 additions & 0 deletions src/runtime/composables/useFormField.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ type Props<T> = {
size?: GetObjectField<T, 'size'>
color?: GetObjectField<T, 'color'>
highlight?: boolean
required?: boolean
disabled?: boolean
}

Expand Down Expand Up @@ -74,6 +75,7 @@ export function useFormField<T>(props?: Props<T>, 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(() => props?.required || formField?.value.required),
disabled: computed(() => formOptions?.value.disabled || props?.disabled),
emitFormBlur,
emitFormInput,
Expand Down
1 change: 1 addition & 0 deletions src/runtime/types/form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ export interface FormFieldInjectedOptions<T> {
validateOnInputDelay?: number
errorPattern?: RegExp
hint?: string
required?: boolean
description?: string
help?: string
ariaId: string
Expand Down
43 changes: 34 additions & 9 deletions test/components/FormField.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
USlider,
UPinInput,
UFormField,
UForm,
UFileUpload
} from '#components'

Expand All @@ -27,18 +28,29 @@ async function renderFormField(options: {
props: Partial<FormFieldProps>
inputComponent: typeof inputComponents[number]
}) {
return await mountSuspended(UFormField, {
props: options.props,
let modelValue: any = '0'
if ((options.inputComponent as any).__name === 'FileUpload') {
modelValue = new File([''], 'test-file.txt', { type: 'text/plain' })
}

return await mountSuspended(UForm, {
slots: {
default: {
// @ts-expect-error - Object literal may only specify known properties, and setup does not exist in type
setup: () => ({ inputComponent: options.inputComponent }),
setup: () => ({
formFieldProps: options.props,
inputComponent: options.inputComponent,
modelValue
}),
components: {
UFormField,
UForm,
...inputComponents
},
template: `
<component :is="inputComponent" />
<UFormField v-bind="formFieldProps">
<component :is="inputComponent" :model-value="modelValue" />
</UFormField>
`
}
}
Expand All @@ -52,11 +64,12 @@ const FormFieldWrapper = defineComponent({
UFormField
},
template: `
<UFormField>
<template v-for="(_, name) in $slots" #[name]="slotData">
<slot :name="name" v-bind="slotData" />
</template>
</UFormField>`
<UFormField>
<template v-for="(_, name) in $slots" #[name]="slotData">
<slot :name="name" v-bind="slotData" />
</template>
</UFormField>
`
})

describe('FormField', () => {
Expand Down Expand Up @@ -118,6 +131,18 @@ describe('FormField', () => {
expect(input.exists()).toBe(true)
})
}
test('binds required', async () => {
const wrapper = await renderFormField({
props: {
required: true,
name
},
inputComponent
})

const requiredInput = wrapper.find('[required], [aria-required=true]')
expect(requiredInput.exists()).toBe(true)
})

test('binds hints with aria-describedby', async () => {
const wrapper = await renderFormField({
Expand Down
Loading
Loading