feat(Form): new component (#4)

Co-authored-by: Benjamin Canac <canacb1@gmail.com>
This commit is contained in:
Romain Hamel
2024-03-19 16:09:12 +01:00
committed by GitHub
parent 1cec712fb8
commit de62676647
35 changed files with 2735 additions and 69 deletions

View File

@@ -18,6 +18,7 @@ module.exports = {
// Typescript
'@typescript-eslint/type-annotation-spacing': 'error',
'@typescript-eslint/semi': ['error', 'never'],
// Vuejs
'vue/multi-word-component-names': 0,

View File

@@ -44,6 +44,8 @@
"@nuxt/test-utils": "^3.12.0",
"@vue/test-utils": "^2.4.5",
"eslint": "^8.57.0",
"joi": "^17.12.2",
"valibot": "^0.30.0",
"happy-dom": "^13.10.1",
"nuxt": "^3.11.1",
"nuxt-ui-dev-module": "workspace:*",
@@ -51,7 +53,9 @@
"vitest-environment-nuxt": "^1.0.0",
"vue": "^3.4.21",
"vue-router": "^4.3.0",
"vue-tsc": "^2.0.6"
"vue-tsc": "^2.0.6",
"yup": "^1.4.0",
"zod": "^3.22.4"
},
"resolutions": {
"nuxt-ui3": "workspace:*"

View File

@@ -1,19 +1,48 @@
<script setup lang="ts">
import { splitByCase, upperFirst } from 'scule'
useHead({
bodyAttrs: {
class: 'antialiased font-sans text-gray-900 dark:text-white bg-white dark:bg-gray-900'
}
})
const components = ['avatar', 'badge', 'button', 'card', 'chip', 'collapsible', 'kbd', 'modal', 'popover', 'skeleton', 'slideover', 'tabs', 'tooltip']
const components = [
'avatar',
'badge',
'button',
'card',
'chip',
'collapsible',
'form',
'form-field',
'input',
'kbd',
'modal',
'popover',
'skeleton',
'slideover',
'tabs',
'tooltip'
]
function upperName (name: string) {
return splitByCase(name).map(p => upperFirst(p)).join('')
}
</script>
<template>
<UProvider>
<UContainer class="min-h-screen flex flex-col gap-4 items-center justify-center overflow-y-auto">
<div class="flex gap-1.5 py-4 overflow-x-auto w-full">
<ULink v-for="component in components" :key="component" :to="`/${component}`" active-class="text-primary-500 dark:text-primary-400 font-medium" class="capitalize text-sm">
{{ component }}
<div class="flex gap-1.5 py-4">
<ULink
v-for="component in components"
:key="component"
:to="`/${component}`"
active-class="text-primary-500 dark:text-primary-400 font-medium"
class="capitalize text-sm"
>
{{ upperName(component) }}
</ULink>
</div>

View File

@@ -2,6 +2,6 @@
export default defineNuxtConfig({
modules: ['../src/module'],
ui: {
colors: ['primary']
colors: ['primary', 'red']
}
})

View File

@@ -1,3 +1,20 @@
<script setup lang="ts">
import chip from '#build/ui/chip'
const sizes = Object.keys(chip.variants.size)
const positions = Object.keys(chip.variants.position)
const items = [{
name: 'messages',
icon: 'i-heroicons-chat-bubble-oval-left',
count: 3
}, {
name: 'notifications',
icon: 'i-heroicons-bell',
count: 0
}]
</script>
<template>
<div class="flex flex-col gap-4">
<div class="flex items-center gap-2">
@@ -19,20 +36,3 @@
</div>
</div>
</template>
<script setup lang="ts">
import chip from '#build/ui/chip'
const sizes = Object.keys(chip.variants.size)
const positions = Object.keys(chip.variants.position)
const items = [{
name: 'messages',
icon: 'i-heroicons-chat-bubble-oval-left',
count: 3
}, {
name: 'notifications',
icon: 'i-heroicons-bell',
count: 0
}]
</script>

View File

@@ -0,0 +1,35 @@
<script setup lang="ts">
import formField from '#build/ui/formField'
const feedbacks = [
{ description: 'This is a description' },
{ error: 'This is an error' },
{ hint: 'This is a hint' },
{ help: 'Help! I need somebody!' },
{ required: true }
]
</script>
<template>
<div class="flex flex-col items-center gap-4">
<div class="flex flex-col gap-4 -ml-[258px]">
<div v-for="(feedback, count) in feedbacks" :key="count" class="flex items-center">
<UFormField v-bind="feedback" label="Email" name="email">
<UInput placeholder="john@lennon.com" />
</UFormField>
</div>
</div>
<div class="flex items-center gap-4">
<UFormField
v-for="size in Object.keys(formField.variants.size)"
:key="size"
:size="(size as any)"
label="Email"
name="email"
>
<UInput placeholder="john@lennon.com" />
</UFormField>
</div>
</div>
</template>

104
playground/pages/form.vue Normal file
View File

@@ -0,0 +1,104 @@
<script setup lang="ts">
import { z } from 'zod'
import type { Form, FormSubmitEvent } from '#ui/types/form'
type User = {
email: string
password: string
}
const state = ref<User>({ email: '', password: '' })
const state2 = ref<User>({ email: '', password: '' })
const state3 = ref<User>({ email: '', password: '' })
const schema = z.object({
email: z.string().email(),
password: z.string().min(8)
})
const disabledForm = ref<Form<User>>()
function onSubmit (event: FormSubmitEvent<User>) {
console.log(event.data)
}
</script>
<template>
<div class="flex gap-4">
<UForm
:state="state"
:schema="schema"
class="gap-4 flex flex-col w-60"
@submit="(event) => onSubmit(event)"
>
<UFormField label="Email" name="email">
<UInput v-model="state.email" placeholder="john@lennon.com" />
</UFormField>
<UFormField label="Password" name="password">
<UInput v-model="state.password" type="password" />
</UFormField>
<div>
<UButton color="gray" type="submit">
Submit
</UButton>
</div>
</UForm>
<UForm
:state="state2"
:schema="schema"
class="gap-4 flex flex-col w-60"
:validate-on-input-delay="2000"
@submit="(event) => onSubmit(event)"
>
<UFormField label="Email" name="email">
<UInput v-model="state2.email" placeholder="john@lennon.com" />
</UFormField>
<UFormField
label="Password"
name="password"
:validate-on-input-delay="50"
eager-validation
>
<UInput v-model="state2.password" type="password" />
</UFormField>
<div>
<UButton color="gray" type="submit">
Submit
</UButton>
</div>
</UForm>
<UForm
ref="disabledForm"
:state="state3"
:schema="schema"
class="gap-4 flex flex-col w-60"
disabled
@submit="(event) => onSubmit(event)"
>
<UFormField label="Email" name="email">
<UInput v-model="state2.email" placeholder="john@lennon.com" />
</UFormField>
<UFormField
label="Password"
name="password"
:validate-on-input-delay="50"
eager-validation
>
<UInput v-model="state2.password" type="password" />
</UFormField>
<div>
<UButton color="gray" type="submit" :disabled="disabledForm?.disabled">
Submit
</UButton>
</div>
</UForm>
</div>
</template>

View File

@@ -0,0 +1,19 @@
<script setup lang="ts">
import input from '#build/ui/input'
</script>
<template>
<div class="flex flex-col items-center gap-4">
<div class="flex flex-col gap-4 -ml-[258px]">
<UInput placeholder="Write something..." autofocus />
<UInput placeholder="Write something..." />
<UInput placeholder="Write something..." color="gray" />
<UInput placeholder="Write something..." color="primary" />
<UInput placeholder="Write something..." color="red" />
<UInput placeholder="Write something..." disabled />
</div>
<div class="flex items-center gap-4">
<UInput v-for="size in Object.keys(input.variants.size)" :key="size" placeholder="Write something..." :size="(size as any)" />
</div>
</div>
</template>

View File

@@ -1,3 +1,7 @@
<script setup lang="ts">
const open = ref(false)
</script>
<template>
<div class="flex flex-col gap-2">
<UModal title="First modal">
@@ -47,7 +51,3 @@
</UModal>
</div>
</template>
<script setup lang="ts">
const open = ref(false)
</script>

View File

@@ -1,3 +1,17 @@
<script setup lang="ts">
const open = ref(false)
const loading = ref(false)
function send () {
loading.value = true
setTimeout(() => {
loading.value = false
open.value = false
}, 1000)
}
</script>
<template>
<div class="text-center">
<UPopover v-model:open="open" arrow>
@@ -48,17 +62,3 @@
</div>
</div>
</template>
<script setup lang="ts">
const open = ref(false)
const loading = ref(false)
function send () {
loading.value = true
setTimeout(() => {
loading.value = false
open.value = false
}, 1000)
}
</script>

View File

@@ -1,3 +1,7 @@
<script setup lang="ts">
const open = ref(false)
</script>
<template>
<div class="flex flex-col gap-2">
<USlideover title="First slideover">
@@ -103,7 +107,3 @@
</USlideover>
</div>
</template>
<script setup lang="ts">
const open = ref(false)
</script>

View File

@@ -1,11 +1,3 @@
<template>
<UTabs :items="items" class="w-96">
<template #tab1="{ item }">
{{ item.label }}
</template>
</UTabs>
</template>
<script setup lang="ts">
const items = [{
label: 'Tab1',
@@ -19,3 +11,11 @@ const items = [{
content: 'Finally, this is the content for Tab3'
}]
</script>
<template>
<UTabs :items="items" class="w-96">
<template #tab1="{ item }">
{{ item.label }}
</template>
</UTabs>
</template>

80
pnpm-lock.yaml generated
View File

@@ -66,12 +66,18 @@ importers:
happy-dom:
specifier: ^13.10.1
version: 13.10.1
joi:
specifier: ^17.12.2
version: 17.12.2
nuxt:
specifier: ^3.11.1
version: 3.11.1(eslint@8.57.0)(rollup@3.29.4)(typescript@5.4.2)(vite@5.1.6)(vue-tsc@2.0.6)
nuxt-ui-dev-module:
specifier: workspace:*
version: link:modules/dev
valibot:
specifier: ^0.30.0
version: 0.30.0
vitest:
specifier: ^1.4.0
version: 1.4.0(happy-dom@13.10.1)
@@ -87,6 +93,12 @@ importers:
vue-tsc:
specifier: ^2.0.6
version: 2.0.6(typescript@5.4.2)
yup:
specifier: ^1.4.0
version: 1.4.0
zod:
specifier: ^3.22.4
version: 3.22.4
cli:
dependencies:
@@ -888,6 +900,16 @@ packages:
- vue
dev: false
/@hapi/hoek@9.3.0:
resolution: {integrity: sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==}
dev: true
/@hapi/topo@5.1.0:
resolution: {integrity: sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==}
dependencies:
'@hapi/hoek': 9.3.0
dev: true
/@humanwhocodes/config-array@0.11.14:
resolution: {integrity: sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==}
engines: {node: '>=10.10.0'}
@@ -1904,6 +1926,20 @@ packages:
resolution: {integrity: sha512-RbhOOTCNoCrbfkRyoXODZp75MlpiHMgbE5MEBZAnnnLyQNgrigEj4p0lzsMDyc1zVsJDLrivB58tgg3emX0eEA==}
dev: true
/@sideway/address@4.1.5:
resolution: {integrity: sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==}
dependencies:
'@hapi/hoek': 9.3.0
dev: true
/@sideway/formula@3.0.1:
resolution: {integrity: sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==}
dev: true
/@sideway/pinpoint@2.0.0:
resolution: {integrity: sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==}
dev: true
/@sigstore/bundle@2.2.0:
resolution: {integrity: sha512-5VI58qgNs76RDrwXNhpmyN/jKpq9evV/7f1XrcqcAfvxDl5SeVY/I5Rmfe96ULAV7/FK5dge9RBKGBJPhL1WsQ==}
engines: {node: ^16.14.0 || >=18.0.0}
@@ -4318,6 +4354,16 @@ packages:
resolution: {integrity: sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==}
hasBin: true
/joi@17.12.2:
resolution: {integrity: sha512-RonXAIzCiHLc8ss3Ibuz45u28GOsWE1UpfDXLbN/9NKbL4tCJf8TWYVKsoYuuh+sAUt7fsSNpA+r2+TBA6Wjmw==}
dependencies:
'@hapi/hoek': 9.3.0
'@hapi/topo': 5.1.0
'@sideway/address': 4.1.5
'@sideway/formula': 3.0.1
'@sideway/pinpoint': 2.0.0
dev: true
/js-beautify@1.15.1:
resolution: {integrity: sha512-ESjNzSlt/sWE8sciZH8kBF8BPlwXPwhR6pWKAw8bw4Bwj+iZcnKW6ONWUutJ7eObuBZQpiIb8S7OYspWrKt7rA==}
engines: {node: '>=14'}
@@ -5834,6 +5880,10 @@ packages:
kleur: 3.0.3
sisteransi: 1.0.5
/property-expr@2.0.6:
resolution: {integrity: sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA==}
dev: true
/proto-list@1.2.4:
resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==}
dev: true
@@ -6475,6 +6525,10 @@ packages:
/text-table@0.2.0:
resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==}
/tiny-case@1.0.3:
resolution: {integrity: sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==}
dev: true
/tiny-invariant@1.3.3:
resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==}
@@ -6506,6 +6560,10 @@ packages:
resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==}
engines: {node: '>=0.6'}
/toposort@2.0.2:
resolution: {integrity: sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==}
dev: true
/totalist@3.0.1:
resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==}
engines: {node: '>=6'}
@@ -6555,6 +6613,11 @@ packages:
resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==}
engines: {node: '>=10'}
/type-fest@2.19.0:
resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==}
engines: {node: '>=12.20'}
dev: true
/type-fest@3.13.1:
resolution: {integrity: sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==}
engines: {node: '>=14.16'}
@@ -6853,6 +6916,10 @@ packages:
/util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
/valibot@0.30.0:
resolution: {integrity: sha512-5POBdbSkM+3nvJ6ZlyQHsggisfRtyT4tVTo1EIIShs6qCdXJnyWU5TJ68vr8iTg5zpOLjXLRiBqNx+9zwZz/rA==}
dev: true
/validate-npm-package-license@3.0.4:
resolution: {integrity: sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==}
dependencies:
@@ -7347,6 +7414,15 @@ packages:
engines: {node: '>=12.20'}
dev: true
/yup@1.4.0:
resolution: {integrity: sha512-wPbgkJRCqIf+OHyiTBQoJiP5PFuAXaWiJK6AmYkzQAh5/c2K9hzSApBZG5wV9KoKSePF7sAxmNSvh/13YHkFDg==}
dependencies:
property-expr: 2.0.6
tiny-case: 1.0.3
toposort: 2.0.2
type-fest: 2.19.0
dev: true
/zhead@2.2.4:
resolution: {integrity: sha512-8F0OI5dpWIA5IGG5NHUg9staDwz/ZPxZtvGVf01j7vHqSyZ0raHY+78atOVxRqb73AotX22uV1pXt3gYSstGag==}
@@ -7357,3 +7433,7 @@ packages:
archiver-utils: 5.0.2
compress-commons: 6.0.2
readable-stream: 4.5.2
/zod@3.22.4:
resolution: {integrity: sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==}
dev: true

View File

@@ -33,9 +33,19 @@ export interface ButtonProps extends LinkProps {
}
export interface ButtonSlots {
leading(props: { disabled?: boolean; loading?: boolean, icon?: string, class: string }): any
leading(props: {
disabled?: boolean
loading?: boolean
icon?: string
class: string
}): any
default(): any
trailing(props: { disabled?: boolean; loading?: boolean, icon?: string, class: string }): any
trailing(props: {
disabled?: boolean
loading?: boolean
icon?: string
class: string
}): any
}
</script>

View File

@@ -0,0 +1,196 @@
<script lang="ts">
import { tv } from 'tailwind-variants'
import type { AppConfig } from '@nuxt/schema'
import _appConfig from '#build/app.config'
import theme from '#build/ui/form'
import { getYupErrors, isYupSchema, getValibotError, isValibotSchema, getZodErrors, isZodSchema, getJoiErrors, isJoiSchema } from '../utils/form'
import type { FormSchema, FormError, FormInputEvents, FormErrorEvent, FormSubmitEvent, FormEvent, InjectedFormOptions, Form } from '../types/form'
const appConfig = _appConfig as AppConfig & { ui: { form: Partial<typeof theme> } }
const form = tv({ extend: tv(theme), ...(appConfig.ui?.form || {}) })
export interface FormProps<T extends object> {
schema?: FormSchema<T>
state: Partial<T>
validate?: (state: Partial<T>) => Promise<FormError[] | void>
validateOn?: FormInputEvents[]
disabled?: boolean
validateOnInputDelay?: number
class?: any
}
export interface FormEmits<T extends object> {
(e: 'submit', payload: FormSubmitEvent<T>): void
(e: 'error', payload: FormErrorEvent): void
}
export interface FormSlots {
default(): any
}
export class FormException extends Error {
constructor (message: string) {
super(message)
this.message = message
Object.setPrototypeOf(this, FormException.prototype)
}
}
</script>
<script lang="ts" setup generic="T extends object">
import { provide, ref, onUnmounted, onMounted, computed } from 'vue'
import { useEventBus } from '@vueuse/core'
import { useId } from '#imports'
const props = withDefaults(defineProps<FormProps<T>>(), {
validateOn () {
return ['input', 'blur', 'change'] as FormInputEvents[]
},
validateOnInputDelay: 300
})
const emit = defineEmits<FormEmits<T>>()
defineSlots<FormSlots>()
const formId = useId()
const bus = useEventBus<FormEvent>(`form-${formId}`)
onMounted(() => {
bus.on(async (event) => {
if (
event.type !== 'submit' &&
props.validateOn?.includes(event.type as FormInputEvents)
) {
await _validate(event.name, { silent: true })
}
})
})
onUnmounted(() => {
bus.reset()
})
const options = {
disabled: computed(() => props.disabled),
validateOnInputDelay: computed(() => props.validateOnInputDelay)
}
provide<InjectedFormOptions>('form-options', options)
const errors = ref<FormError[]>([])
provide('form-errors', errors)
provide('form-events', bus)
const inputs = ref<Record<string, string>>({})
provide('form-inputs', inputs)
async function getErrors (): Promise<FormError[]> {
let errs = props.validate ? (await props.validate(props.state)) ?? [] : []
if (props.schema) {
if (isZodSchema(props.schema)) {
errs = errs.concat(await getZodErrors(props.state, props.schema))
} else if (isYupSchema(props.schema)) {
errs = errs.concat(await getYupErrors(props.state, props.schema))
} else if (isJoiSchema(props.schema)) {
errs = errs.concat(await getJoiErrors(props.state, props.schema))
} else if (isValibotSchema(props.schema)) {
errs = errs.concat(await getValibotError(props.state, props.schema))
} else {
throw new Error('Form validation failed: Unsupported form schema')
}
}
return errs
}
async function _validate (
name?: string | string[],
opts: { silent?: boolean } = { silent: false }
): Promise<T | false> {
let paths = name
if (name && !Array.isArray(name)) {
paths = [name]
}
if (paths) {
const otherErrors = errors.value.filter(
(error) => !paths!.includes(error.name)
)
const pathErrors = (await getErrors()).filter((error) =>
paths!.includes(error.name)
)
errors.value = otherErrors.concat(pathErrors)
} else {
errors.value = await getErrors()
}
if (errors.value.length > 0) {
if (opts.silent) return false
throw new FormException(`Form validation failed: ${JSON.stringify(errors.value, null, 2)}`)
}
return props.state as T
}
async function onSubmit (payload: Event) {
const event = payload as SubmitEvent
try {
await _validate()
const submitEvent: FormSubmitEvent<any> = {
...event,
data: props.state
}
emit('submit', submitEvent)
} catch (error) {
if (!(error instanceof FormException)) {
throw error
}
const errorEvent: FormErrorEvent = {
...event,
errors: errors.value.map((err) => ({
...err,
id: inputs.value[err.name]
}))
}
emit('error', errorEvent)
}
}
defineExpose<Form<T>>({
validate: _validate,
errors,
setErrors (errs: FormError[], name?: string) {
errors.value = errs
if (name) {
errors.value = errors.value
.filter((error) => error.name !== name)
.concat(errs)
} else {
errors.value = errs
}
},
async submit () {
await onSubmit(new Event('submit'))
},
getErrors (name?: string) {
if (name) {
return errors.value.filter((err) => err.name === name)
}
return errors.value
},
clear (name?: string) {
if (name) {
errors.value = errors.value.filter((err) => err.name !== name)
} else {
errors.value = []
}
},
...options
})
</script>
<template>
<form :class="form({ class: props.class })" @submit.prevent="onSubmit">
<slot />
</form>
</template>

View File

@@ -0,0 +1,132 @@
<script lang="ts">
import { tv, type VariantProps } from 'tailwind-variants'
import type { AppConfig } from '@nuxt/schema'
import _appConfig from '#build/app.config'
import theme from '#build/ui/formField'
const appConfig = _appConfig as AppConfig & { ui: { formField: Partial<typeof theme> } }
const formField = tv({ extend: tv(theme), ...(appConfig.ui?.formField || {}) })
type FormFieldVariants = VariantProps<typeof formField>
export interface FormFieldProps {
name?: string
label?: string
description?: string
help?: string
error?: string
hint?: string
size?: FormFieldVariants['size']
required?: boolean
eagerValidation?: boolean
validateOnInputDelay?: number
class?: any
ui?: Partial<typeof formField.slots>
}
export interface FormFieldSlots {
label(props: { label?: string }): any
hint(props: { hint?: string }): any
description(props: { description?: string }): any
error(props: { error?: string }): any
help(props: { help?: string }): any
default(props: { error?: string }): any
}
</script>
<script lang="ts" setup>
import { computed, ref, inject, provide, type Ref } from 'vue'
import type { FormError, InjectedFormFieldOptions } from '../types/form'
import { useId } from '#imports'
const props = defineProps<FormFieldProps>()
defineSlots<FormFieldSlots>()
const ui = computed(() => tv({ extend: formField, slots: props.ui })({
size: props.size,
required: props.required
}))
const formErrors = inject<Ref<FormError[]> | null>('form-errors', null)
const error = computed(() => {
return (props.error && typeof props.error === 'string') ||
typeof props.error === 'boolean'
? props.error
: formErrors?.value?.find((error) => error.name === props.name)?.message
})
const inputId = ref(useId())
provide<InjectedFormFieldOptions>('form-field', {
error,
inputId,
name: computed(() => props.name),
size: computed(() => props.size),
eagerValidation: computed(() => props.eagerValidation),
validateOnInputDelay: computed(() => props.validateOnInputDelay)
})
</script>
<template>
<div :class="ui.root({ class: props.class })">
<div :class="ui.wrapper()">
<div v-if="label || $slots.label" :class="ui.labelWrapper()">
<label :for="inputId" :class="ui.label()">
<slot name="label" :label="label">
{{ label }}
</slot>
</label>
<span v-if="hint || $slots.hint" :class="ui.hint()">
<slot name="hint" :hint="hint">
{{ hint }}
</slot>
</span>
</div>
<p v-if="description || $slots.description" :class="ui.description()">
<slot name="description" :description="description">
{{ description }}
</slot>
</p>
</div>
<div :class="label ? ui.container() : ''">
<slot :error="error" />
<Transition name="slide-fade" mode="out-in">
<p v-if="(typeof error === 'string' && error) || $slots.error" :class="ui.error()">
<slot name="error" :error="error">
{{ error }}
</slot>
</p>
<p v-else-if="help || $slots.help" :class="ui.help()">
<slot name="help" :help="help">
{{ help }}
</slot>
</p>
</Transition>
</div>
</div>
</template>
<style scoped>
.slide-fade-enter-active {
transition: all 0.15s ease-out;
}
.slide-fade-leave-active {
transition: all 0.15s ease-out;
}
.slide-fade-enter-from {
transform: translateY(-10px);
opacity: 0;
}
.slide-fade-leave-to {
transform: translateY(10px);
opacity: 0;
}
</style>

View File

@@ -0,0 +1,176 @@
<script lang="ts">
// TODO: Add missing props / slots (e.g. icons)
import { tv, type VariantProps } from 'tailwind-variants'
import { defu } from 'defu'
import type { AppConfig } from '@nuxt/schema'
import _appConfig from '#build/app.config'
import theme from '#build/ui/input'
import { looseToNumber } from '../utils'
const appConfig = _appConfig as AppConfig & { ui: { input: Partial<typeof theme> } }
const input = tv({ extend: tv(theme), ...(appConfig.ui?.input || {}) })
type InputVariants = VariantProps<typeof input>
export interface InputProps {
id?: string | number
name?: string
type?: string
required?: boolean
color?: InputVariants['color']
variant?: InputVariants['variant']
size?: InputVariants['size']
modelValue?: string
placeholder?: string
autofocus?: boolean
autofocusDelay?: number
modelModifiers?: {
trim?: boolean
lazy?: boolean
number?: boolean
}
disabled?: boolean
class?: any
ui?: Partial<typeof input.slots>
}
export interface InputEmits {
(e: 'update:modelValue', value: string): void
(e: 'blur', event: FocusEvent): void
}
export interface InputSlots {
leading(props: {
disabled?: boolean
loading?: boolean
icon?: string
class: string
}): any
default(): any
trailing(props: {
disabled?: boolean
loading?: boolean
icon?: string
class: string
}): any
}
</script>
<script lang="ts" setup>
import { ref, computed, onMounted } from 'vue'
import { useFormField } from '../composables/useFormField'
defineOptions({ inheritAttrs: false })
const props = withDefaults(defineProps<InputProps>(), {
type: 'text',
autofocusDelay: 100
})
const emit = defineEmits<InputEmits>()
defineSlots<InputSlots>()
const { emitFormBlur, emitFormInput, size, color, inputId, name, disabled } = useFormField(props)
const ui = computed(() => tv({ extend: input, slots: props.ui })({
color: color.value,
variant: props.variant,
size: size?.value
}))
// const { size: sizeButtonGroup, rounded } = useInjectButtonGroup({ ui, props })
// const size = computed(() => sizeButtonGroup.value || sizeFormGroup.value)
const modelModifiers = ref(defu({}, props.modelModifiers, { trim: false, lazy: false, number: false }))
const inputRef = ref<HTMLInputElement | null>(null)
const autoFocus = () => {
if (props.autofocus) {
inputRef.value?.focus()
}
}
// Custom function to handle the v-model properties
const updateInput = (value: string) => {
if (modelModifiers.value.trim) {
value = value.trim()
}
if (modelModifiers.value.number || props.type === 'number') {
value = looseToNumber(value)
}
emit('update:modelValue', value)
emitFormInput()
}
const onInput = (event: Event) => {
if (!modelModifiers.value.lazy) {
updateInput((event.target as HTMLInputElement).value)
}
}
const onChange = (event: Event) => {
const value = (event.target as HTMLInputElement).value
if (modelModifiers.value.lazy) {
updateInput(value)
}
// Update trimmed input so that it has same behavior as native input https://github.com/vuejs/core/blob/5ea8a8a4fab4e19a71e123e4d27d051f5e927172/packages/runtime-dom/src/directives/vModel.ts#L63
if (modelModifiers.value.trim) {
(event.target as HTMLInputElement).value = value.trim()
}
}
const onBlur = (event: FocusEvent) => {
emitFormBlur()
emit('blur', event)
}
onMounted(() => {
setTimeout(() => {
autoFocus()
}, props.autofocusDelay)
})
</script>
<template>
<div :class="ui.root({ class: props.class })">
<input
:id="inputId"
ref="inputRef"
:type="type"
:value="modelValue"
:name="name"
:placeholder="placeholder"
:class="ui.base()"
:disabled="disabled"
v-bind="$attrs"
@input="onInput"
@blur="onBlur"
@change="onChange"
>
<slot />
<!-- span
v-if="(isLeading && leadingIconName) || $slots.leading"
:class="leadingWrapperIconClass"
>
<slot name="leading" :disabled="disabled" :loading="loading">
<UIcon :name="leadingIconName" :class="leadingIconClass" />
</slot>
</span>
<span
v-if="(isTrailing && trailingIconName) || $slots.trailing"
:class="trailingWrapperIconClass"
>
<slot name="trailing" :disabled="disabled" :loading="loading">
<UIcon :name="trailingIconName" :class="trailingIconClass" />
</slot>
</span -->
</div>
</template>

View File

@@ -17,16 +17,21 @@ export interface TabsItem {
content?: string
}
export interface TabsProps extends Omit<TabsRootProps, 'asChild'> {
items: TabsItem[]
export interface TabsProps<T extends TabsItem> extends Omit<TabsRootProps, 'asChild'> {
items: T[]
class?: any
ui?: Partial<typeof tabs.slots>
}
export interface TabsEmits extends TabsRootEmits {}
export interface TabsSlots {
type SlotFunction<T> = (props: { item: T, index: number }) => any
export type TabsSlots<T extends TabsItem> = {
default(): any
item(): SlotFunction<T>
} & {
[key in T['slot'] as string]?: SlotFunction<T>
}
</script>
@@ -34,16 +39,9 @@ export interface TabsSlots {
import { computed } from 'vue'
import { TabsRoot, TabsList, TabsIndicator, TabsTrigger, TabsContent, useForwardPropsEmits } from 'radix-vue'
const props = withDefaults(defineProps<TabsProps & { items: T[] }>(), { defaultValue: '0' })
const props = withDefaults(defineProps<TabsProps<T>>(), { defaultValue: '0' })
const emits = defineEmits<TabsEmits>()
type SlotFunction<T> = (props: { item: T, index: number }) => any
defineSlots<TabsSlots & {
item(): SlotFunction<T>
} & {
[key in T['slot'] as string]?: SlotFunction<T>
}>()
defineSlots<TabsSlots<T>>()
const rootProps = useForwardPropsEmits(props, emits)

View File

@@ -0,0 +1,70 @@
import { inject, ref, computed } from 'vue'
import { type UseEventBusReturn, useDebounceFn } from '@vueuse/core'
import type { FormEvent, FormInputEvents, InjectedFormFieldOptions, InjectedFormOptions } from '../types/form'
type InputProps = {
id?: string | number
size?: string | number | symbol
color?: string // FIXME: Replace by enum
name?: string
eagerValidation?: boolean
legend?: string | null
disabled?: boolean | null
}
export const useFormField = (inputProps?: InputProps) => {
const formOptions = inject<InjectedFormOptions | undefined>('form-options', undefined)
const formBus = inject<UseEventBusReturn<FormEvent, string> | undefined>('form-events', undefined)
const formField = inject<InjectedFormFieldOptions | undefined>('form-field', undefined)
const formInputs = inject<any>('form-inputs', undefined)
if (formField) {
if (inputProps?.id) {
// Updates for="..." attribute on label if inputProps.id is provided
formField.inputId.value = inputProps?.id
}
if (formInputs) {
formInputs.value[formField.name.value] = formField.inputId.value
}
}
const blurred = ref(false)
function emitFormEvent (type: FormInputEvents, name: string) {
if (formBus && formField) {
formBus.emit({ type, name })
}
}
function emitFormBlur () {
emitFormEvent('blur', formField?.name.value as string)
blurred.value = true
}
function emitFormChange () {
emitFormEvent('change', formField?.name.value as string)
}
const emitFormInput = useDebounceFn(
() => {
if (blurred.value || formField?.eagerValidation.value) {
emitFormEvent('input', formField?.name.value as string)
}
},
formField?.validateOnInputDelay.value ??
formOptions?.validateOnInputDelay.value ??
0
)
return {
inputId: computed(() => inputProps?.id ?? formField?.inputId.value),
name: computed(() => inputProps?.name ?? formField?.name.value),
size: computed(() => inputProps?.size ?? formField?.size?.value),
color: computed(() => formField?.error?.value ? 'red' : inputProps?.color),
disabled: computed(() => formOptions?.disabled?.value || inputProps?.disabled),
emitFormBlur,
emitFormInput,
emitFormChange
}
}

51
src/runtime/types/form.d.ts vendored Normal file
View File

@@ -0,0 +1,51 @@
export interface Form<T> {
validate (path?: string | string[], opts?: { silent?: true }): Promise<T | false>
validate (path?: string | string[], opts?: { silent?: false }): Promise<T | false>
clear (path?: string): void
errors: Ref<FormError[]>
setErrors (errs: FormError[], path?: string): void
getErrors (path?: string): FormError[]
submit (): Promise<void>
disabled: ComputedRef<boolean>
}
export type FormSchema<T extends object> =
| ZodSchema
| YupObjectSchema<T>
| ValibotObjectSchema<T>
| JoiSchema<T>
export type FormInputEvents = 'input' | 'blur' | 'change'
export interface FormError<P extends string = string> {
name: P
message: string
}
export interface FormErrorWithId extends FormError {
id: string
}
export type FormSubmitEvent<T> = SubmitEvent & { data: T }
export type FormErrorEvent = SubmitEvent & { errors: FormErrorWithId[] }
export type FormEventType = FormInputEvents | 'submit'
export interface FormEvent {
type: FormEventType
name?: string
}
export interface InjectedFormFieldOptions {
inputId: Ref<string | number | undefined>
name: Computed<string>
size: Computed<string | number | symbol>
error: Computed<string | boolean | undefined>
eagerValidation: Computed<boolean>
validateOnInputDelay: Computed<number | undefined>
}
export interface InjectedFormOptions {
disabled?: Computed<boolean>
validateOnInputDelay?: Computed<number>
}

View File

@@ -0,0 +1 @@

83
src/runtime/utils/form.ts Normal file
View File

@@ -0,0 +1,83 @@
import type { ZodSchema } from 'zod'
import type { ValidationError as JoiError, Schema as JoiSchema } from 'joi'
import type { ObjectSchema as YupObjectSchema, ValidationError as YupError } from 'yup'
import type { ObjectSchemaAsync as ValibotObjectSchema } from 'valibot'
import type { FormError } from '../types/form'
export function isYupSchema (schema: any): schema is YupObjectSchema<any> {
return schema.validate && schema.__isYupSchema__
}
export function isYupError (error: any): error is YupError {
return error.inner !== undefined
}
export async function getYupErrors (state: any, schema: YupObjectSchema<any>): Promise<FormError[]> {
try {
await schema.validate(state, { abortEarly: false })
return []
} catch (error) {
if (isYupError(error)) {
return error.inner.map((issue) => ({
name: issue.path ?? '',
message: issue.message
}))
} else {
throw error
}
}
}
export function isZodSchema (schema: any): schema is ZodSchema {
return schema.parse !== undefined
}
export async function getZodErrors (state: any, schema: ZodSchema): Promise<FormError[]> {
const result = await schema.safeParseAsync(state)
if (result.success === false) {
return result.error.issues.map((issue) => ({
name: issue.path.join('.'),
message: issue.message
}))
}
return []
}
export function isJoiSchema (schema: any): schema is JoiSchema {
return schema.validateAsync !== undefined && schema.id !== undefined
}
export function isJoiError (error: any): error is JoiError {
return error.isJoi === true
}
export async function getJoiErrors (state: any, schema: JoiSchema): Promise<FormError[]> {
try {
await schema.validateAsync(state, { abortEarly: false })
return []
} catch (error) {
if (isJoiError(error)) {
return error.details.map((detail) => ({
name: detail.path.join('.'),
message: detail.message
}))
} else {
throw error
}
}
}
export function isValibotSchema (schema: any): schema is ValibotObjectSchema<any> {
return schema._parse !== undefined
}
export async function getValibotError (state: any, schema: ValibotObjectSchema<any>): Promise<FormError[]> {
const result = await schema._parse(state)
if (result.issues) {
return result.issues.map((issue) => ({
name: issue.path?.map((p) => p.key).join('.') || '',
message: issue.message
}))
}
return []
}

View File

@@ -0,0 +1,4 @@
export function looseToNumber (val: any): any {
const n = parseFloat(val)
return isNaN(n) ? val : n
}

3
src/theme/form.ts Normal file
View File

@@ -0,0 +1,3 @@
export default {
base: ''
}

32
src/theme/formField.ts Normal file
View File

@@ -0,0 +1,32 @@
export default {
slots: {
root: '',
wrapper: '',
labelWrapper: 'flex content-center items-center justify-between',
label: 'block font-medium text-gray-700 dark:text-gray-200',
container: 'mt-1 relative',
description: 'text-gray-500 dark:text-gray-400',
error: 'mt-2 text-red-500 dark:text-red-400',
hint: 'text-gray-500 dark:text-gray-400',
help: 'mt-2 text-gray-500 dark:text-gray-400'
},
variants: {
size: {
'2xs': { root: 'text-xs' },
xs: { root: 'text-xs' },
sm: { root: 'text-sm' },
md: { root: 'text-sm' },
lg: { root: 'text-sm' },
xl: { root: 'text-base' }
},
required: {
true: {
// eslint-disable-next-line quotes
label: `after:content-['*'] after:ms-0.5 after:text-red-500 dark:after:text-red-400`
}
}
},
defaultVariants: {
size: 'sm'
}
}

View File

@@ -5,7 +5,10 @@ export { default as card } from './card'
export { default as chip } from './chip'
export { default as collapsible } from './collapsible'
export { default as container } from './container'
export { default as form } from './form'
export { default as formField } from './formField'
export { default as icons } from './icons'
export { default as input } from './input'
export { default as kbd } from './kbd'
export { default as modal } from './modal'
export { default as popover } from './popover'

66
src/theme/input.ts Normal file
View File

@@ -0,0 +1,66 @@
export default (config: { colors: string[] }) => {
return {
slots: {
root: 'relative',
base: 'relative block w-full disabled:cursor-not-allowed disabled:opacity-75 focus:outline-none border-0 rounded-md placeholder-gray-400 dark:placeholder-gray-500',
icon: '',
leading: '',
trailing: ''
},
variants: {
size: {
'2xs': {
base: 'text-xs gap-x-1 px-2 py-1',
icon: 'h-4 w-4'
},
xs: {
base: 'text-sm gap-x-1.5 px-2.5 py-1.5',
icon: 'size-4'
},
sm: {
base: 'text-sm gap-x-1.5 px-2.5 py-1.5',
icon: 'size-5'
},
md: {
base: 'text-sm gap-x-1.5 px-3 py-2',
icon: 'size-5'
},
lg: {
base: 'text-sm gap-x-2.5 px-3.5 py-2.5',
icon: 'size-5'
},
xl: {
base: 'text-base gap-x-2.5 px-3.5 py-2.5',
icon: 'size-6'
}
},
variant: {
outline: '',
none: 'bg-transparent focus:ring-0 focus:shadow-none'
},
color: {
...Object.fromEntries(config.colors.map((color: string) => [color, ''])),
white: '',
gray: ''
}
},
compoundVariants: [...config.colors.map((color: string) => ({
color,
variant: 'outline',
class: `shadow-sm bg-transparent text-gray-900 dark:text-white ring-1 ring-inset ring-${color}-500 dark:ring-${color}-400 focus:ring-2 focus:ring-${color}-500 dark:focus:ring-${color}-400`
})), {
color: 'white',
variant: 'outline',
class: 'shadow-sm bg-white dark:bg-gray-900 text-gray-900 dark:text-white ring-1 ring-inset ring-gray-300 dark:ring-gray-700 focus:ring-2 focus:ring-primary-500 dark:focus:ring-primary-400'
}, {
color: 'gray',
variant: 'outline',
class: 'shadow-sm bg-gray-50 dark:bg-gray-800 text-gray-900 dark:text-white ring-1 ring-inset ring-gray-300 dark:ring-gray-700 focus:ring-2 focus:ring-primary-500 dark:focus:ring-primary-400'
}],
defaultVariants: {
size: 'sm',
color: 'white',
variant: 'outline'
}
}
}

View File

@@ -3,7 +3,7 @@ import path from 'path'
export default async function (nameOrHtml: string, options: any, component: any) {
let html: string
const name = path.parse(component.__file).name
const name = component.__file ? path.parse(component.__file).name : undefined
if (options === undefined) {
const app = {
template: nameOrHtml,

View File

@@ -0,0 +1,450 @@
import { reactive } from 'vue'
import { describe, it, expect } from 'vitest'
import type { FormProps } from '../../src/runtime/components/Form.vue'
import {
UForm,
UInput,
UFormField
// URadioGroup,
// UTextarea,
// UCheckbox,
// USelect,
// URadio,
// USelectMenu,
// UInputMenu,
// UToggle,
// URange
} from '#components'
import { DOMWrapper, flushPromises, VueWrapper } from '@vue/test-utils'
import ComponentRender from '../component-render'
import { mountSuspended } from '@nuxt/test-utils/runtime'
import { z } from 'zod'
import * as yup from 'yup'
import Joi from 'joi'
import * as valibot from 'valibot'
async function triggerEvent (
el: DOMWrapper<Element> | VueWrapper<any, any>,
event: string
) {
el.trigger(event)
return flushPromises()
}
async function setValue (
el: DOMWrapper<Element> | VueWrapper<any, any>,
value: any
) {
el.setValue(value)
return flushPromises()
}
async function renderForm (options: {
props: Partial<FormProps<any>>
slotVars?: object
slotComponents?: any
slotTemplate: string
}) {
const state = reactive({})
return await mountSuspended(UForm, {
props: {
state,
...options.props
},
slots: {
default: {
// @ts-ignore
setup () {
return { state, ...options.slotVars }
},
components: {
UFormField,
...options.slotComponents
},
template: options.slotTemplate
}
}
})
}
describe('Form', () => {
it.each([
['basic case', { props: { state: {} } }],
['with default slot', { props: { state: {} }, slots: { default: 'Form slot' } }]
])('renders %s correctly', async (nameOrHtml: string, options: { props: FormProps<any> }) => {
const html = await ComponentRender(nameOrHtml, options, UForm)
expect(html).toMatchSnapshot()
})
it.each([
['zod', {
schema: z.object({
email: z.string(),
password: z.string().min(8, 'Must be at least 8 characters')
})
}
],
['yup', {
schema: yup.object({
email: yup.string(),
password: yup.string().min(8, 'Must be at least 8 characters')
})
}
],
['joi', {
schema: Joi.object({
email: Joi.string(),
password: Joi.string().min(8).messages({
'string.min': 'Must be at least {#limit} characters'
})
})
}
],
['valibot', {
schema: valibot.objectAsync({
email: valibot.string(),
password: valibot.string([
valibot.minLength(8, 'Must be at least 8 characters')
])
})
}
],
['custom', {
async validate (state: any) {
const errs = []
if (!state.email)
errs.push({ name: 'email', message: 'Email is required' })
if (state.password?.length < 8)
errs.push({
name: 'password',
message: 'Must be at least 8 characters'
})
return errs
}
}
]
])('%s validation works', async (_nameOrHtml: string, options: Partial<FormProps<any>>) => {
const wrapper = await renderForm({
props: options,
slotComponents: {
UFormField,
UInput
},
slotTemplate: `
<UFormField name="email">
<UInput id="email" v-model="state.email" />
</UFormField>
<UFormField name="password">
<UInput id="password" v-model="state.password" />
</UFormField>
`
})
const form = wrapper.find('form')
const emailInput = wrapper.find('#email')
const passwordInput = wrapper.find('#password')
await setValue(emailInput, 'bob@dylan.com')
await setValue(passwordInput, 'short')
await triggerEvent(form, 'submit.prevent')
// @ts-ignore
expect(wrapper.emitted('error')[0][0].errors).toMatchObject([
{
id: 'password',
name: 'password',
message: 'Must be at least 8 characters'
}
])
expect(wrapper.html()).toMatchSnapshot('with error')
await setValue(passwordInput, 'validpassword')
await triggerEvent(form, 'submit.prevent')
expect(wrapper.emitted()).toHaveProperty('submit')
expect(wrapper.emitted('submit')![0][0]).toMatchObject({
data: { email: 'bob@dylan.com', password: 'validpassword' }
})
expect(wrapper.html()).toMatchSnapshot('without error')
})
it.each([
['input', UInput, {}, 'foo']
// ['textarea', UTextarea, {}, 'foo']
])('%s validate on blur works', async (_name, InputComponent, inputProps, validInputValue) => {
const wrapper = await renderForm({
props: {
validateOn: ['blur'],
async validate (state: any) {
if (!state.value)
return [{ name: 'value', message: 'Error message' }]
return []
}
},
slotVars: {
inputProps
},
slotComponents: { InputComponent },
slotTemplate: `
<UFormField name="value">
<InputComponent id="input" v-model="state.value" v-bind="inputProps" />
</UFormField>
`
})
const input = wrapper.find('#input')
await triggerEvent(input, 'blur')
expect(wrapper.text()).toContain('Error message')
await setValue(input, validInputValue)
await triggerEvent(input, 'blur')
expect(wrapper.text()).not.toContain('Error message')
})
it.each([
// ['checkbox', UCheckbox, {}, true],
// ['range', URange, {}, 2],
// ['select', USelect, { options: ['Option 1', 'Option 2'] }, 'Option 2']
])('%s validate on change works', async (_name, InputComponent, inputProps, validInputValue) => {
const wrapper = await renderForm({
props: {
validateOn: ['change'],
async validate (state: any) {
if (!state.value)
return [{ name: 'value', message: 'Error message' }]
return []
}
},
slotVars: {
inputProps
},
slotComponents: {
InputComponent
},
slotTemplate: `
<UFormField name="value">
<InputComponent id="input" v-model="state.value" v-bind="inputProps" />
</UFormField>
`
})
const input = wrapper.find('#input')
await triggerEvent(input, 'change')
expect(wrapper.text()).toContain('Error message')
await setValue(input, validInputValue)
await triggerEvent(input, 'change')
expect(wrapper.text()).not.toContain('Error message')
})
// test('radio group validate on change works', async () => {
// const wrapper = await renderForm({
// props: {
// validateOn: ['change'],
// validate (state: any) {
// if (state.value !== 'Option 2')
// return [{ name: 'value', message: 'Error message' }]
// return []
// }
// },
// slotVars: {
// inputProps: {
// options: ['Option 1', 'Option 2', 'Option 3']
// }
// },
// slotComponents: {
// UFormField,
// URadioGroup
// },
// slotTemplate: `
// <UFormField name="value">
// <URadioGroup id="input" v-model="state.value" v-bind="inputProps" />
// </UFormField>
// `
// })
//
// const option1 = wrapper.find('[value="Option 1"]')
// await setValue(option1, true)
// expect(wrapper.text()).toContain('Error message')
//
// const option2 = wrapper.find('[value="Option 2"]')
// await setValue(option2, true)
// expect(wrapper.text()).not.toContain('Error message')
// })
//
// test('radio validate on change works', async () => {
// const wrapper = await renderForm({
// props: {
// validateOn: ['change'],
// validate (state: any) {
// if (state.value !== 'Option 2')
// return [{ name: 'value', message: 'Error message' }]
// return []
// }
// },
// slotComponents: {
// UFormField,
// URadio
// },
// slotTemplate: `
// <UFormField name="value">
// <URadio id="option-1" v-model="state.value" value="Option 1" />
// <URadio id="option-2" v-model="state.value" value="Option 2" />
// </UFormField>
// `
// })
//
// const option1 = wrapper.find('#option-1')
// await setValue(option1, true)
// expect(wrapper.text()).toContain('Error message')
//
// const option2 = wrapper.find('#option-2')
// await setValue(option2, true)
// expect(wrapper.text()).not.toContain('Error message')
// })
//
// test('toggle validate on change', async () => {
// const wrapper = await renderForm({
// props: {
// validateOn: ['change'],
// validate (state: any) {
// if (state.value) return [{ name: 'value', message: 'Error message' }]
// return []
// }
// },
// slotComponents: {
// UFormField,
// UToggle
// },
// slotTemplate: `
// <UFormField name="value">
// <UToggle id="input" v-model="state.value" />
// </UFormField>
// `
// })
//
// const input = wrapper.findComponent({ name: 'Switch' })
// await setValue(input, true)
// expect(wrapper.text()).toContain('Error message')
//
// await setValue(input, false)
// expect(wrapper.text()).not.toContain('Error message')
// })
//
// test('select menu validate on change', async () => {
// const wrapper = await renderForm({
// props: {
// validateOn: ['change'],
// validate (state: any) {
// if (state.value !== 'Option 2')
// return [{ name: 'value', message: 'Error message' }]
// return []
// }
// },
// slotVars: {
// inputProps: {
// options: ['Option 1', 'Option 2', 'Option 3']
// }
// },
// slotComponents: {
// UFormField,
// USelectMenu
// },
// slotTemplate: `
// <UFormField name="value">
// <USelectMenu id="input" v-model="state.value" v-bind="inputProps" />
// </UFormField>
// `
// })
//
// const input = wrapper.findComponent({ name: 'Listbox' })
// await setValue(input, 'Option 1')
// expect(wrapper.text()).toContain('Error message')
//
// await setValue(input, 'Option 2')
// expect(wrapper.text()).not.toContain('Error message')
// })
//
// test('input menu validate on change', async () => {
// const wrapper = await renderForm({
// props: {
// validateOn: ['change'],
// validate (state: any) {
// if (state.value !== 'Option 2')
// return [{ name: 'value', message: 'Error message' }]
// return []
// }
// },
// slotVars: {
// inputProps: {
// options: ['Option 1', 'Option 2', 'Option 3']
// }
// },
// slotComponents: {
// UFormField,
// UInputMenu
// },
// slotTemplate: `
// <UFormField name="value">
// <UInputMenu id="input" v-model="state.value" v-bind="inputProps" />
// </UFormField>
// `
// })
//
// const input = wrapper.findComponent({ name: 'Combobox' })
// await setValue(input, 'Option 1')
// expect(wrapper.text()).toContain('Error message')
//
// await setValue(input, 'Option 2')
// expect(wrapper.text()).not.toContain('Error message')
// })
//
it.each([
['input', UInput, {}, 'foo']
// ['textarea', UTextarea, {}, 'foo']
])('%s validate on input works', async (_name, InputComponent, inputProps, validInputValue) => {
const wrapper = await renderForm({
props: {
validateOn: ['input', 'blur'],
async validate (state: any) {
if (!state.value)
return [{ name: 'value', message: 'Error message' }]
return []
}
},
slotVars: {
inputProps
},
slotComponents: {
UFormField,
InputComponent
},
slotTemplate: `
<UFormField name="value" :validate-on-input-delay="50">
<InputComponent id="input" v-model="state.value" v-bind="inputProps" />
</UFormField>
`
})
const input = wrapper.find('#input')
// Validation @input is enabled only after a blur event
await triggerEvent(input, 'blur')
expect(wrapper.text()).toContain('Error message')
await setValue(input, validInputValue)
// Waiting because of the debounced validation on input event.
await new Promise((r) => setTimeout(r, 50))
expect(wrapper.text()).not.toContain('Error message')
})
})

View File

@@ -0,0 +1,35 @@
import { defineComponent } from 'vue'
import { describe, it, expect } from 'vitest'
import FormField, { type FormFieldProps } from '../../src/runtime/components/FormField.vue'
import ComponentRender from '../component-render'
// A wrapper component is needed here because of a conflict with the error prop / expose.
// See: https://github.com/nuxt/test-utils/issues/684
const FormFieldWrapper = defineComponent({
components: {
UFormField: FormField
},
template: '<UFormField v-bind="$attrs"> <slot /> </UFormField>'
})
describe('FormField', () => {
it.each([
['with label and description', { props: { label: 'Username', description: 'Enter your username' } }],
['with size', { props: { label: 'Username', description: 'Enter your username', size: 'xl' as const } }],
['with required', { props: { label: 'Username', required: true } }],
['with help', { props: { help: 'Username must be unique' } }],
['with error', { props: { error: 'Username is already taken' } }],
['with hint', { props: { hint: 'Use letters, numbers, and special characters' } }],
['with class', { props: { class: 'relative' } }],
['with ui', { props: { ui: { label: 'text-gray-900 dark:text-white' } } }],
['with default slot', { slots: { default: () => 'Default slot' } }],
['with label slot', { slots: { label: () => 'Label slot' } }],
['with description slot', { slots: { description: () => 'Description slot' } }],
['with error slot', { slots: { error: () => 'Error slot' } }],
['with hint slot', { slots: { hint: () => 'Hint slot' } }],
['with help slot', { slots: { help: () => 'Help slot' } }]
])('renders %s correctly', async (nameOrHtml: string, options: { props?: FormFieldProps, slots?: any }) => {
const html = await ComponentRender(nameOrHtml, options, FormFieldWrapper)
expect(html).toMatchSnapshot()
})
})

View File

@@ -0,0 +1,28 @@
import { describe, it, expect } from 'vitest'
import Input, { type InputProps } from '../../src/runtime/components/Input.vue'
import ComponentRender from '../component-render'
describe('Input', () => {
it.each([
['basic case', {}],
['with name', { props: { name: 'username' } }],
['with type', { props: { type: 'password' } }],
['with placeholder', { props: { placeholder: 'Enter your username' } }],
['with disabled', { props: { disabled: true } }],
['with required', { props: { required: true } }],
// ['with icon', { props: { icon: 'i-heroicons-battery-50-solid' } }],
// ['with leading and icon', { props: { leading: true, icon: 'i-heroicons-battery-50-solid' } }],
// ['with leadingIcon', { props: { leadingIcon: 'i-heroicons-battery-50-solid' } }],
// ['with loading icon', { props: { loading: true } }],
// ['with leading slot', { slots: { leading: () => 'leading slot' } }],
// ['with trailing and icon', { props: { trailing: true, icon: 'i-heroicons-battery-50-solid' } }],
// ['with trailingIcon', { props: { trailingIcon: 'i-heroicons-battery-50-solid' } }],
// ['with trailing slot', { slots: { leading: () => 'trailing slot' } }],
['with size', { props: { size: 'xs' as const } }],
['with color', { props: { color: 'red' as const } }],
['with variant', { props: { variant: 'outline' as const } }]
])('renders %s correctly', async (nameOrHtml: string, options: { props?: InputProps, slots?: any }) => {
const html = await ComponentRender(nameOrHtml, options, Input)
expect(html).toMatchSnapshot()
})
})

View File

@@ -23,7 +23,7 @@ describe('Tabs', () => {
['with defaultValue', { props: { items, defaultValue: '1' } }],
['with default slot', { props: { items }, slots: { default: () => 'Default slot' } }],
['with item slot', { props: { items }, slots: { item: () => 'Item slot' } }]
])('renders %s correctly', async (nameOrHtml: string, options: { props?: TabsProps, slots?: any }) => {
])('renders %s correctly', async (nameOrHtml: string, options: { props?: TabsProps<typeof items[number]>, slots?: any }) => {
const html = await ComponentRender(nameOrHtml, options, Tabs)
expect(html).toMatchSnapshot()
})

View File

@@ -0,0 +1,655 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`Form > custom validation works > with error 1`] = `
"<form class="">
<div data-v-0fd934a4="" class="text-sm">
<div data-v-0fd934a4="" class="">
<!--v-if-->
<!--v-if-->
</div>
<div data-v-0fd934a4="" class="">
<div class="relative"><input id="email" type="text" name="email" class="relative block w-full disabled:cursor-not-allowed disabled:opacity-75 focus:outline-none border-0 rounded-md placeholder-gray-400 dark:placeholder-gray-500 text-sm gap-x-1.5 px-2.5 py-1.5 shadow-sm bg-white dark:bg-gray-900 text-gray-900 dark:text-white ring-1 ring-inset ring-gray-300 dark:ring-gray-700 focus:ring-2 focus:ring-primary-500 dark:focus:ring-primary-400">
<!-- span
v-if="(isLeading && leadingIconName) || $slots.leading"
:class="leadingWrapperIconClass"
>
<slot name="leading" :disabled="disabled" :loading="loading">
<UIcon :name="leadingIconName" :class="leadingIconClass" />
</slot>
</span>
<span
v-if="(isTrailing && trailingIconName) || $slots.trailing"
:class="trailingWrapperIconClass"
>
<slot name="trailing" :disabled="disabled" :loading="loading">
<UIcon :name="trailingIconName" :class="trailingIconClass" />
</slot>
</span -->
</div>
<transition-stub data-v-0fd934a4="" name="slide-fade" mode="out-in" appear="false" persisted="false" css="true">
<!--v-if-->
</transition-stub>
</div>
</div>
<div data-v-0fd934a4="" class="text-sm">
<div data-v-0fd934a4="" class="">
<!--v-if-->
<!--v-if-->
</div>
<div data-v-0fd934a4="" class="">
<div class="relative"><input id="password" type="text" name="password" class="relative block w-full disabled:cursor-not-allowed disabled:opacity-75 focus:outline-none border-0 rounded-md placeholder-gray-400 dark:placeholder-gray-500 text-sm gap-x-1.5 px-2.5 py-1.5 shadow-sm bg-transparent text-gray-900 dark:text-white ring-1 ring-inset ring-red-500 dark:ring-red-400 focus:ring-2 focus:ring-red-500 dark:focus:ring-red-400">
<!-- span
v-if="(isLeading && leadingIconName) || $slots.leading"
:class="leadingWrapperIconClass"
>
<slot name="leading" :disabled="disabled" :loading="loading">
<UIcon :name="leadingIconName" :class="leadingIconClass" />
</slot>
</span>
<span
v-if="(isTrailing && trailingIconName) || $slots.trailing"
:class="trailingWrapperIconClass"
>
<slot name="trailing" :disabled="disabled" :loading="loading">
<UIcon :name="trailingIconName" :class="trailingIconClass" />
</slot>
</span -->
</div>
<transition-stub data-v-0fd934a4="" name="slide-fade" mode="out-in" appear="false" persisted="false" css="true">
<p data-v-0fd934a4="" class="mt-2 text-red-500 dark:text-red-400">Must be at least 8 characters</p>
</transition-stub>
</div>
</div>
</form>"
`;
exports[`Form > custom validation works > without error 1`] = `
"<form class="">
<div data-v-0fd934a4="" class="text-sm">
<div data-v-0fd934a4="" class="">
<!--v-if-->
<!--v-if-->
</div>
<div data-v-0fd934a4="" class="">
<div class="relative"><input id="email" type="text" name="email" class="relative block w-full disabled:cursor-not-allowed disabled:opacity-75 focus:outline-none border-0 rounded-md placeholder-gray-400 dark:placeholder-gray-500 text-sm gap-x-1.5 px-2.5 py-1.5 shadow-sm bg-white dark:bg-gray-900 text-gray-900 dark:text-white ring-1 ring-inset ring-gray-300 dark:ring-gray-700 focus:ring-2 focus:ring-primary-500 dark:focus:ring-primary-400">
<!-- span
v-if="(isLeading && leadingIconName) || $slots.leading"
:class="leadingWrapperIconClass"
>
<slot name="leading" :disabled="disabled" :loading="loading">
<UIcon :name="leadingIconName" :class="leadingIconClass" />
</slot>
</span>
<span
v-if="(isTrailing && trailingIconName) || $slots.trailing"
:class="trailingWrapperIconClass"
>
<slot name="trailing" :disabled="disabled" :loading="loading">
<UIcon :name="trailingIconName" :class="trailingIconClass" />
</slot>
</span -->
</div>
<transition-stub data-v-0fd934a4="" name="slide-fade" mode="out-in" appear="false" persisted="false" css="true">
<!--v-if-->
</transition-stub>
</div>
</div>
<div data-v-0fd934a4="" class="text-sm">
<div data-v-0fd934a4="" class="">
<!--v-if-->
<!--v-if-->
</div>
<div data-v-0fd934a4="" class="">
<div class="relative"><input id="password" type="text" name="password" class="relative block w-full disabled:cursor-not-allowed disabled:opacity-75 focus:outline-none border-0 rounded-md placeholder-gray-400 dark:placeholder-gray-500 text-sm gap-x-1.5 px-2.5 py-1.5 shadow-sm bg-white dark:bg-gray-900 text-gray-900 dark:text-white ring-1 ring-inset ring-gray-300 dark:ring-gray-700 focus:ring-2 focus:ring-primary-500 dark:focus:ring-primary-400">
<!-- span
v-if="(isLeading && leadingIconName) || $slots.leading"
:class="leadingWrapperIconClass"
>
<slot name="leading" :disabled="disabled" :loading="loading">
<UIcon :name="leadingIconName" :class="leadingIconClass" />
</slot>
</span>
<span
v-if="(isTrailing && trailingIconName) || $slots.trailing"
:class="trailingWrapperIconClass"
>
<slot name="trailing" :disabled="disabled" :loading="loading">
<UIcon :name="trailingIconName" :class="trailingIconClass" />
</slot>
</span -->
</div>
<transition-stub data-v-0fd934a4="" name="slide-fade" mode="out-in" appear="false" persisted="false" css="true">
<!--v-if-->
</transition-stub>
</div>
</div>
</form>"
`;
exports[`Form > joi validation works > with error 1`] = `
"<form class="">
<div data-v-0fd934a4="" class="text-sm">
<div data-v-0fd934a4="" class="">
<!--v-if-->
<!--v-if-->
</div>
<div data-v-0fd934a4="" class="">
<div class="relative"><input id="email" type="text" name="email" class="relative block w-full disabled:cursor-not-allowed disabled:opacity-75 focus:outline-none border-0 rounded-md placeholder-gray-400 dark:placeholder-gray-500 text-sm gap-x-1.5 px-2.5 py-1.5 shadow-sm bg-white dark:bg-gray-900 text-gray-900 dark:text-white ring-1 ring-inset ring-gray-300 dark:ring-gray-700 focus:ring-2 focus:ring-primary-500 dark:focus:ring-primary-400">
<!-- span
v-if="(isLeading && leadingIconName) || $slots.leading"
:class="leadingWrapperIconClass"
>
<slot name="leading" :disabled="disabled" :loading="loading">
<UIcon :name="leadingIconName" :class="leadingIconClass" />
</slot>
</span>
<span
v-if="(isTrailing && trailingIconName) || $slots.trailing"
:class="trailingWrapperIconClass"
>
<slot name="trailing" :disabled="disabled" :loading="loading">
<UIcon :name="trailingIconName" :class="trailingIconClass" />
</slot>
</span -->
</div>
<transition-stub data-v-0fd934a4="" name="slide-fade" mode="out-in" appear="false" persisted="false" css="true">
<!--v-if-->
</transition-stub>
</div>
</div>
<div data-v-0fd934a4="" class="text-sm">
<div data-v-0fd934a4="" class="">
<!--v-if-->
<!--v-if-->
</div>
<div data-v-0fd934a4="" class="">
<div class="relative"><input id="password" type="text" name="password" class="relative block w-full disabled:cursor-not-allowed disabled:opacity-75 focus:outline-none border-0 rounded-md placeholder-gray-400 dark:placeholder-gray-500 text-sm gap-x-1.5 px-2.5 py-1.5 shadow-sm bg-transparent text-gray-900 dark:text-white ring-1 ring-inset ring-red-500 dark:ring-red-400 focus:ring-2 focus:ring-red-500 dark:focus:ring-red-400">
<!-- span
v-if="(isLeading && leadingIconName) || $slots.leading"
:class="leadingWrapperIconClass"
>
<slot name="leading" :disabled="disabled" :loading="loading">
<UIcon :name="leadingIconName" :class="leadingIconClass" />
</slot>
</span>
<span
v-if="(isTrailing && trailingIconName) || $slots.trailing"
:class="trailingWrapperIconClass"
>
<slot name="trailing" :disabled="disabled" :loading="loading">
<UIcon :name="trailingIconName" :class="trailingIconClass" />
</slot>
</span -->
</div>
<transition-stub data-v-0fd934a4="" name="slide-fade" mode="out-in" appear="false" persisted="false" css="true">
<p data-v-0fd934a4="" class="mt-2 text-red-500 dark:text-red-400">Must be at least 8 characters</p>
</transition-stub>
</div>
</div>
</form>"
`;
exports[`Form > joi validation works > without error 1`] = `
"<form class="">
<div data-v-0fd934a4="" class="text-sm">
<div data-v-0fd934a4="" class="">
<!--v-if-->
<!--v-if-->
</div>
<div data-v-0fd934a4="" class="">
<div class="relative"><input id="email" type="text" name="email" class="relative block w-full disabled:cursor-not-allowed disabled:opacity-75 focus:outline-none border-0 rounded-md placeholder-gray-400 dark:placeholder-gray-500 text-sm gap-x-1.5 px-2.5 py-1.5 shadow-sm bg-white dark:bg-gray-900 text-gray-900 dark:text-white ring-1 ring-inset ring-gray-300 dark:ring-gray-700 focus:ring-2 focus:ring-primary-500 dark:focus:ring-primary-400">
<!-- span
v-if="(isLeading && leadingIconName) || $slots.leading"
:class="leadingWrapperIconClass"
>
<slot name="leading" :disabled="disabled" :loading="loading">
<UIcon :name="leadingIconName" :class="leadingIconClass" />
</slot>
</span>
<span
v-if="(isTrailing && trailingIconName) || $slots.trailing"
:class="trailingWrapperIconClass"
>
<slot name="trailing" :disabled="disabled" :loading="loading">
<UIcon :name="trailingIconName" :class="trailingIconClass" />
</slot>
</span -->
</div>
<transition-stub data-v-0fd934a4="" name="slide-fade" mode="out-in" appear="false" persisted="false" css="true">
<!--v-if-->
</transition-stub>
</div>
</div>
<div data-v-0fd934a4="" class="text-sm">
<div data-v-0fd934a4="" class="">
<!--v-if-->
<!--v-if-->
</div>
<div data-v-0fd934a4="" class="">
<div class="relative"><input id="password" type="text" name="password" class="relative block w-full disabled:cursor-not-allowed disabled:opacity-75 focus:outline-none border-0 rounded-md placeholder-gray-400 dark:placeholder-gray-500 text-sm gap-x-1.5 px-2.5 py-1.5 shadow-sm bg-white dark:bg-gray-900 text-gray-900 dark:text-white ring-1 ring-inset ring-gray-300 dark:ring-gray-700 focus:ring-2 focus:ring-primary-500 dark:focus:ring-primary-400">
<!-- span
v-if="(isLeading && leadingIconName) || $slots.leading"
:class="leadingWrapperIconClass"
>
<slot name="leading" :disabled="disabled" :loading="loading">
<UIcon :name="leadingIconName" :class="leadingIconClass" />
</slot>
</span>
<span
v-if="(isTrailing && trailingIconName) || $slots.trailing"
:class="trailingWrapperIconClass"
>
<slot name="trailing" :disabled="disabled" :loading="loading">
<UIcon :name="trailingIconName" :class="trailingIconClass" />
</slot>
</span -->
</div>
<transition-stub data-v-0fd934a4="" name="slide-fade" mode="out-in" appear="false" persisted="false" css="true">
<!--v-if-->
</transition-stub>
</div>
</div>
</form>"
`;
exports[`Form > renders basic case correctly 1`] = `"<form class=""></form>"`;
exports[`Form > renders with default slot correctly 1`] = `"<form class="">Form slot</form>"`;
exports[`Form > valibot validation works > with error 1`] = `
"<form class="">
<div data-v-0fd934a4="" class="text-sm">
<div data-v-0fd934a4="" class="">
<!--v-if-->
<!--v-if-->
</div>
<div data-v-0fd934a4="" class="">
<div class="relative"><input id="email" type="text" name="email" class="relative block w-full disabled:cursor-not-allowed disabled:opacity-75 focus:outline-none border-0 rounded-md placeholder-gray-400 dark:placeholder-gray-500 text-sm gap-x-1.5 px-2.5 py-1.5 shadow-sm bg-white dark:bg-gray-900 text-gray-900 dark:text-white ring-1 ring-inset ring-gray-300 dark:ring-gray-700 focus:ring-2 focus:ring-primary-500 dark:focus:ring-primary-400">
<!-- span
v-if="(isLeading && leadingIconName) || $slots.leading"
:class="leadingWrapperIconClass"
>
<slot name="leading" :disabled="disabled" :loading="loading">
<UIcon :name="leadingIconName" :class="leadingIconClass" />
</slot>
</span>
<span
v-if="(isTrailing && trailingIconName) || $slots.trailing"
:class="trailingWrapperIconClass"
>
<slot name="trailing" :disabled="disabled" :loading="loading">
<UIcon :name="trailingIconName" :class="trailingIconClass" />
</slot>
</span -->
</div>
<transition-stub data-v-0fd934a4="" name="slide-fade" mode="out-in" appear="false" persisted="false" css="true">
<!--v-if-->
</transition-stub>
</div>
</div>
<div data-v-0fd934a4="" class="text-sm">
<div data-v-0fd934a4="" class="">
<!--v-if-->
<!--v-if-->
</div>
<div data-v-0fd934a4="" class="">
<div class="relative"><input id="password" type="text" name="password" class="relative block w-full disabled:cursor-not-allowed disabled:opacity-75 focus:outline-none border-0 rounded-md placeholder-gray-400 dark:placeholder-gray-500 text-sm gap-x-1.5 px-2.5 py-1.5 shadow-sm bg-transparent text-gray-900 dark:text-white ring-1 ring-inset ring-red-500 dark:ring-red-400 focus:ring-2 focus:ring-red-500 dark:focus:ring-red-400">
<!-- span
v-if="(isLeading && leadingIconName) || $slots.leading"
:class="leadingWrapperIconClass"
>
<slot name="leading" :disabled="disabled" :loading="loading">
<UIcon :name="leadingIconName" :class="leadingIconClass" />
</slot>
</span>
<span
v-if="(isTrailing && trailingIconName) || $slots.trailing"
:class="trailingWrapperIconClass"
>
<slot name="trailing" :disabled="disabled" :loading="loading">
<UIcon :name="trailingIconName" :class="trailingIconClass" />
</slot>
</span -->
</div>
<transition-stub data-v-0fd934a4="" name="slide-fade" mode="out-in" appear="false" persisted="false" css="true">
<p data-v-0fd934a4="" class="mt-2 text-red-500 dark:text-red-400">Must be at least 8 characters</p>
</transition-stub>
</div>
</div>
</form>"
`;
exports[`Form > valibot validation works > without error 1`] = `
"<form class="">
<div data-v-0fd934a4="" class="text-sm">
<div data-v-0fd934a4="" class="">
<!--v-if-->
<!--v-if-->
</div>
<div data-v-0fd934a4="" class="">
<div class="relative"><input id="email" type="text" name="email" class="relative block w-full disabled:cursor-not-allowed disabled:opacity-75 focus:outline-none border-0 rounded-md placeholder-gray-400 dark:placeholder-gray-500 text-sm gap-x-1.5 px-2.5 py-1.5 shadow-sm bg-white dark:bg-gray-900 text-gray-900 dark:text-white ring-1 ring-inset ring-gray-300 dark:ring-gray-700 focus:ring-2 focus:ring-primary-500 dark:focus:ring-primary-400">
<!-- span
v-if="(isLeading && leadingIconName) || $slots.leading"
:class="leadingWrapperIconClass"
>
<slot name="leading" :disabled="disabled" :loading="loading">
<UIcon :name="leadingIconName" :class="leadingIconClass" />
</slot>
</span>
<span
v-if="(isTrailing && trailingIconName) || $slots.trailing"
:class="trailingWrapperIconClass"
>
<slot name="trailing" :disabled="disabled" :loading="loading">
<UIcon :name="trailingIconName" :class="trailingIconClass" />
</slot>
</span -->
</div>
<transition-stub data-v-0fd934a4="" name="slide-fade" mode="out-in" appear="false" persisted="false" css="true">
<!--v-if-->
</transition-stub>
</div>
</div>
<div data-v-0fd934a4="" class="text-sm">
<div data-v-0fd934a4="" class="">
<!--v-if-->
<!--v-if-->
</div>
<div data-v-0fd934a4="" class="">
<div class="relative"><input id="password" type="text" name="password" class="relative block w-full disabled:cursor-not-allowed disabled:opacity-75 focus:outline-none border-0 rounded-md placeholder-gray-400 dark:placeholder-gray-500 text-sm gap-x-1.5 px-2.5 py-1.5 shadow-sm bg-white dark:bg-gray-900 text-gray-900 dark:text-white ring-1 ring-inset ring-gray-300 dark:ring-gray-700 focus:ring-2 focus:ring-primary-500 dark:focus:ring-primary-400">
<!-- span
v-if="(isLeading && leadingIconName) || $slots.leading"
:class="leadingWrapperIconClass"
>
<slot name="leading" :disabled="disabled" :loading="loading">
<UIcon :name="leadingIconName" :class="leadingIconClass" />
</slot>
</span>
<span
v-if="(isTrailing && trailingIconName) || $slots.trailing"
:class="trailingWrapperIconClass"
>
<slot name="trailing" :disabled="disabled" :loading="loading">
<UIcon :name="trailingIconName" :class="trailingIconClass" />
</slot>
</span -->
</div>
<transition-stub data-v-0fd934a4="" name="slide-fade" mode="out-in" appear="false" persisted="false" css="true">
<!--v-if-->
</transition-stub>
</div>
</div>
</form>"
`;
exports[`Form > yup validation works > with error 1`] = `
"<form class="">
<div data-v-0fd934a4="" class="text-sm">
<div data-v-0fd934a4="" class="">
<!--v-if-->
<!--v-if-->
</div>
<div data-v-0fd934a4="" class="">
<div class="relative"><input id="email" type="text" name="email" class="relative block w-full disabled:cursor-not-allowed disabled:opacity-75 focus:outline-none border-0 rounded-md placeholder-gray-400 dark:placeholder-gray-500 text-sm gap-x-1.5 px-2.5 py-1.5 shadow-sm bg-white dark:bg-gray-900 text-gray-900 dark:text-white ring-1 ring-inset ring-gray-300 dark:ring-gray-700 focus:ring-2 focus:ring-primary-500 dark:focus:ring-primary-400">
<!-- span
v-if="(isLeading && leadingIconName) || $slots.leading"
:class="leadingWrapperIconClass"
>
<slot name="leading" :disabled="disabled" :loading="loading">
<UIcon :name="leadingIconName" :class="leadingIconClass" />
</slot>
</span>
<span
v-if="(isTrailing && trailingIconName) || $slots.trailing"
:class="trailingWrapperIconClass"
>
<slot name="trailing" :disabled="disabled" :loading="loading">
<UIcon :name="trailingIconName" :class="trailingIconClass" />
</slot>
</span -->
</div>
<transition-stub data-v-0fd934a4="" name="slide-fade" mode="out-in" appear="false" persisted="false" css="true">
<!--v-if-->
</transition-stub>
</div>
</div>
<div data-v-0fd934a4="" class="text-sm">
<div data-v-0fd934a4="" class="">
<!--v-if-->
<!--v-if-->
</div>
<div data-v-0fd934a4="" class="">
<div class="relative"><input id="password" type="text" name="password" class="relative block w-full disabled:cursor-not-allowed disabled:opacity-75 focus:outline-none border-0 rounded-md placeholder-gray-400 dark:placeholder-gray-500 text-sm gap-x-1.5 px-2.5 py-1.5 shadow-sm bg-transparent text-gray-900 dark:text-white ring-1 ring-inset ring-red-500 dark:ring-red-400 focus:ring-2 focus:ring-red-500 dark:focus:ring-red-400">
<!-- span
v-if="(isLeading && leadingIconName) || $slots.leading"
:class="leadingWrapperIconClass"
>
<slot name="leading" :disabled="disabled" :loading="loading">
<UIcon :name="leadingIconName" :class="leadingIconClass" />
</slot>
</span>
<span
v-if="(isTrailing && trailingIconName) || $slots.trailing"
:class="trailingWrapperIconClass"
>
<slot name="trailing" :disabled="disabled" :loading="loading">
<UIcon :name="trailingIconName" :class="trailingIconClass" />
</slot>
</span -->
</div>
<transition-stub data-v-0fd934a4="" name="slide-fade" mode="out-in" appear="false" persisted="false" css="true">
<p data-v-0fd934a4="" class="mt-2 text-red-500 dark:text-red-400">Must be at least 8 characters</p>
</transition-stub>
</div>
</div>
</form>"
`;
exports[`Form > yup validation works > without error 1`] = `
"<form class="">
<div data-v-0fd934a4="" class="text-sm">
<div data-v-0fd934a4="" class="">
<!--v-if-->
<!--v-if-->
</div>
<div data-v-0fd934a4="" class="">
<div class="relative"><input id="email" type="text" name="email" class="relative block w-full disabled:cursor-not-allowed disabled:opacity-75 focus:outline-none border-0 rounded-md placeholder-gray-400 dark:placeholder-gray-500 text-sm gap-x-1.5 px-2.5 py-1.5 shadow-sm bg-white dark:bg-gray-900 text-gray-900 dark:text-white ring-1 ring-inset ring-gray-300 dark:ring-gray-700 focus:ring-2 focus:ring-primary-500 dark:focus:ring-primary-400">
<!-- span
v-if="(isLeading && leadingIconName) || $slots.leading"
:class="leadingWrapperIconClass"
>
<slot name="leading" :disabled="disabled" :loading="loading">
<UIcon :name="leadingIconName" :class="leadingIconClass" />
</slot>
</span>
<span
v-if="(isTrailing && trailingIconName) || $slots.trailing"
:class="trailingWrapperIconClass"
>
<slot name="trailing" :disabled="disabled" :loading="loading">
<UIcon :name="trailingIconName" :class="trailingIconClass" />
</slot>
</span -->
</div>
<transition-stub data-v-0fd934a4="" name="slide-fade" mode="out-in" appear="false" persisted="false" css="true">
<!--v-if-->
</transition-stub>
</div>
</div>
<div data-v-0fd934a4="" class="text-sm">
<div data-v-0fd934a4="" class="">
<!--v-if-->
<!--v-if-->
</div>
<div data-v-0fd934a4="" class="">
<div class="relative"><input id="password" type="text" name="password" class="relative block w-full disabled:cursor-not-allowed disabled:opacity-75 focus:outline-none border-0 rounded-md placeholder-gray-400 dark:placeholder-gray-500 text-sm gap-x-1.5 px-2.5 py-1.5 shadow-sm bg-white dark:bg-gray-900 text-gray-900 dark:text-white ring-1 ring-inset ring-gray-300 dark:ring-gray-700 focus:ring-2 focus:ring-primary-500 dark:focus:ring-primary-400">
<!-- span
v-if="(isLeading && leadingIconName) || $slots.leading"
:class="leadingWrapperIconClass"
>
<slot name="leading" :disabled="disabled" :loading="loading">
<UIcon :name="leadingIconName" :class="leadingIconClass" />
</slot>
</span>
<span
v-if="(isTrailing && trailingIconName) || $slots.trailing"
:class="trailingWrapperIconClass"
>
<slot name="trailing" :disabled="disabled" :loading="loading">
<UIcon :name="trailingIconName" :class="trailingIconClass" />
</slot>
</span -->
</div>
<transition-stub data-v-0fd934a4="" name="slide-fade" mode="out-in" appear="false" persisted="false" css="true">
<!--v-if-->
</transition-stub>
</div>
</div>
</form>"
`;
exports[`Form > zod validation works > with error 1`] = `
"<form class="">
<div data-v-0fd934a4="" class="text-sm">
<div data-v-0fd934a4="" class="">
<!--v-if-->
<!--v-if-->
</div>
<div data-v-0fd934a4="" class="">
<div class="relative"><input id="email" type="text" name="email" class="relative block w-full disabled:cursor-not-allowed disabled:opacity-75 focus:outline-none border-0 rounded-md placeholder-gray-400 dark:placeholder-gray-500 text-sm gap-x-1.5 px-2.5 py-1.5 shadow-sm bg-white dark:bg-gray-900 text-gray-900 dark:text-white ring-1 ring-inset ring-gray-300 dark:ring-gray-700 focus:ring-2 focus:ring-primary-500 dark:focus:ring-primary-400">
<!-- span
v-if="(isLeading && leadingIconName) || $slots.leading"
:class="leadingWrapperIconClass"
>
<slot name="leading" :disabled="disabled" :loading="loading">
<UIcon :name="leadingIconName" :class="leadingIconClass" />
</slot>
</span>
<span
v-if="(isTrailing && trailingIconName) || $slots.trailing"
:class="trailingWrapperIconClass"
>
<slot name="trailing" :disabled="disabled" :loading="loading">
<UIcon :name="trailingIconName" :class="trailingIconClass" />
</slot>
</span -->
</div>
<transition-stub data-v-0fd934a4="" name="slide-fade" mode="out-in" appear="false" persisted="false" css="true">
<!--v-if-->
</transition-stub>
</div>
</div>
<div data-v-0fd934a4="" class="text-sm">
<div data-v-0fd934a4="" class="">
<!--v-if-->
<!--v-if-->
</div>
<div data-v-0fd934a4="" class="">
<div class="relative"><input id="password" type="text" name="password" class="relative block w-full disabled:cursor-not-allowed disabled:opacity-75 focus:outline-none border-0 rounded-md placeholder-gray-400 dark:placeholder-gray-500 text-sm gap-x-1.5 px-2.5 py-1.5 shadow-sm bg-transparent text-gray-900 dark:text-white ring-1 ring-inset ring-red-500 dark:ring-red-400 focus:ring-2 focus:ring-red-500 dark:focus:ring-red-400">
<!-- span
v-if="(isLeading && leadingIconName) || $slots.leading"
:class="leadingWrapperIconClass"
>
<slot name="leading" :disabled="disabled" :loading="loading">
<UIcon :name="leadingIconName" :class="leadingIconClass" />
</slot>
</span>
<span
v-if="(isTrailing && trailingIconName) || $slots.trailing"
:class="trailingWrapperIconClass"
>
<slot name="trailing" :disabled="disabled" :loading="loading">
<UIcon :name="trailingIconName" :class="trailingIconClass" />
</slot>
</span -->
</div>
<transition-stub data-v-0fd934a4="" name="slide-fade" mode="out-in" appear="false" persisted="false" css="true">
<p data-v-0fd934a4="" class="mt-2 text-red-500 dark:text-red-400">Must be at least 8 characters</p>
</transition-stub>
</div>
</div>
</form>"
`;
exports[`Form > zod validation works > without error 1`] = `
"<form class="">
<div data-v-0fd934a4="" class="text-sm">
<div data-v-0fd934a4="" class="">
<!--v-if-->
<!--v-if-->
</div>
<div data-v-0fd934a4="" class="">
<div class="relative"><input id="email" type="text" name="email" class="relative block w-full disabled:cursor-not-allowed disabled:opacity-75 focus:outline-none border-0 rounded-md placeholder-gray-400 dark:placeholder-gray-500 text-sm gap-x-1.5 px-2.5 py-1.5 shadow-sm bg-white dark:bg-gray-900 text-gray-900 dark:text-white ring-1 ring-inset ring-gray-300 dark:ring-gray-700 focus:ring-2 focus:ring-primary-500 dark:focus:ring-primary-400">
<!-- span
v-if="(isLeading && leadingIconName) || $slots.leading"
:class="leadingWrapperIconClass"
>
<slot name="leading" :disabled="disabled" :loading="loading">
<UIcon :name="leadingIconName" :class="leadingIconClass" />
</slot>
</span>
<span
v-if="(isTrailing && trailingIconName) || $slots.trailing"
:class="trailingWrapperIconClass"
>
<slot name="trailing" :disabled="disabled" :loading="loading">
<UIcon :name="trailingIconName" :class="trailingIconClass" />
</slot>
</span -->
</div>
<transition-stub data-v-0fd934a4="" name="slide-fade" mode="out-in" appear="false" persisted="false" css="true">
<!--v-if-->
</transition-stub>
</div>
</div>
<div data-v-0fd934a4="" class="text-sm">
<div data-v-0fd934a4="" class="">
<!--v-if-->
<!--v-if-->
</div>
<div data-v-0fd934a4="" class="">
<div class="relative"><input id="password" type="text" name="password" class="relative block w-full disabled:cursor-not-allowed disabled:opacity-75 focus:outline-none border-0 rounded-md placeholder-gray-400 dark:placeholder-gray-500 text-sm gap-x-1.5 px-2.5 py-1.5 shadow-sm bg-white dark:bg-gray-900 text-gray-900 dark:text-white ring-1 ring-inset ring-gray-300 dark:ring-gray-700 focus:ring-2 focus:ring-primary-500 dark:focus:ring-primary-400">
<!-- span
v-if="(isLeading && leadingIconName) || $slots.leading"
:class="leadingWrapperIconClass"
>
<slot name="leading" :disabled="disabled" :loading="loading">
<UIcon :name="leadingIconName" :class="leadingIconClass" />
</slot>
</span>
<span
v-if="(isTrailing && trailingIconName) || $slots.trailing"
:class="trailingWrapperIconClass"
>
<slot name="trailing" :disabled="disabled" :loading="loading">
<UIcon :name="trailingIconName" :class="trailingIconClass" />
</slot>
</span -->
</div>
<transition-stub data-v-0fd934a4="" name="slide-fade" mode="out-in" appear="false" persisted="false" css="true">
<!--v-if-->
</transition-stub>
</div>
</div>
</form>"
`;

View File

@@ -0,0 +1,202 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`FormField > renders with class correctly 1`] = `
"<div data-v-0fd934a4="" class="text-sm relative">
<div data-v-0fd934a4="" class="">
<!--v-if-->
<!--v-if-->
</div>
<div data-v-0fd934a4="" class="">
<transition-stub data-v-0fd934a4="" name="slide-fade" mode="out-in" appear="false" persisted="false" css="true">
<!--v-if-->
</transition-stub>
</div>
</div>"
`;
exports[`FormField > renders with default slot correctly 1`] = `
"<div data-v-0fd934a4="" class="text-sm">
<div data-v-0fd934a4="" class="">
<!--v-if-->
<!--v-if-->
</div>
<div data-v-0fd934a4="" class="">Default slot<transition-stub data-v-0fd934a4="" name="slide-fade" mode="out-in" appear="false" persisted="false" css="true">
<!--v-if-->
</transition-stub>
</div>
</div>"
`;
exports[`FormField > renders with description slot correctly 1`] = `
"<div data-v-0fd934a4="" class="text-sm">
<div data-v-0fd934a4="" class="">
<!--v-if-->
<!--v-if-->
</div>
<div data-v-0fd934a4="" class="">
<transition-stub data-v-0fd934a4="" name="slide-fade" mode="out-in" appear="false" persisted="false" css="true">
<!--v-if-->
</transition-stub>
</div>
</div>"
`;
exports[`FormField > renders with error correctly 1`] = `
"<div data-v-0fd934a4="" class="text-sm">
<div data-v-0fd934a4="" class="">
<!--v-if-->
<!--v-if-->
</div>
<div data-v-0fd934a4="" class="">
<transition-stub data-v-0fd934a4="" name="slide-fade" mode="out-in" appear="false" persisted="false" css="true">
<p data-v-0fd934a4="" class="mt-2 text-red-500 dark:text-red-400">Username is already taken</p>
</transition-stub>
</div>
</div>"
`;
exports[`FormField > renders with error slot correctly 1`] = `
"<div data-v-0fd934a4="" class="text-sm">
<div data-v-0fd934a4="" class="">
<!--v-if-->
<!--v-if-->
</div>
<div data-v-0fd934a4="" class="">
<transition-stub data-v-0fd934a4="" name="slide-fade" mode="out-in" appear="false" persisted="false" css="true">
<!--v-if-->
</transition-stub>
</div>
</div>"
`;
exports[`FormField > renders with help correctly 1`] = `
"<div data-v-0fd934a4="" class="text-sm">
<div data-v-0fd934a4="" class="">
<!--v-if-->
<!--v-if-->
</div>
<div data-v-0fd934a4="" class="">
<transition-stub data-v-0fd934a4="" name="slide-fade" mode="out-in" appear="false" persisted="false" css="true">
<p data-v-0fd934a4="" class="mt-2 text-gray-500 dark:text-gray-400">Username must be unique</p>
</transition-stub>
</div>
</div>"
`;
exports[`FormField > renders with help slot correctly 1`] = `
"<div data-v-0fd934a4="" class="text-sm">
<div data-v-0fd934a4="" class="">
<!--v-if-->
<!--v-if-->
</div>
<div data-v-0fd934a4="" class="">
<transition-stub data-v-0fd934a4="" name="slide-fade" mode="out-in" appear="false" persisted="false" css="true">
<!--v-if-->
</transition-stub>
</div>
</div>"
`;
exports[`FormField > renders with hint correctly 1`] = `
"<div data-v-0fd934a4="" class="text-sm">
<div data-v-0fd934a4="" class="">
<!--v-if-->
<!--v-if-->
</div>
<div data-v-0fd934a4="" class="">
<transition-stub data-v-0fd934a4="" name="slide-fade" mode="out-in" appear="false" persisted="false" css="true">
<!--v-if-->
</transition-stub>
</div>
</div>"
`;
exports[`FormField > renders with hint slot correctly 1`] = `
"<div data-v-0fd934a4="" class="text-sm">
<div data-v-0fd934a4="" class="">
<!--v-if-->
<!--v-if-->
</div>
<div data-v-0fd934a4="" class="">
<transition-stub data-v-0fd934a4="" name="slide-fade" mode="out-in" appear="false" persisted="false" css="true">
<!--v-if-->
</transition-stub>
</div>
</div>"
`;
exports[`FormField > renders with label and description correctly 1`] = `
"<div data-v-0fd934a4="" class="text-sm">
<div data-v-0fd934a4="" class="">
<div data-v-0fd934a4="" class="flex content-center items-center justify-between"><label data-v-0fd934a4="" for="0" class="block font-medium text-gray-700 dark:text-gray-200">Username</label>
<!--v-if-->
</div>
<p data-v-0fd934a4="" class="text-gray-500 dark:text-gray-400">Enter your username</p>
</div>
<div data-v-0fd934a4="" class="mt-1 relative">
<transition-stub data-v-0fd934a4="" name="slide-fade" mode="out-in" appear="false" persisted="false" css="true">
<!--v-if-->
</transition-stub>
</div>
</div>"
`;
exports[`FormField > renders with label slot correctly 1`] = `
"<div data-v-0fd934a4="" class="text-sm">
<div data-v-0fd934a4="" class="">
<!--v-if-->
<!--v-if-->
</div>
<div data-v-0fd934a4="" class="">
<transition-stub data-v-0fd934a4="" name="slide-fade" mode="out-in" appear="false" persisted="false" css="true">
<!--v-if-->
</transition-stub>
</div>
</div>"
`;
exports[`FormField > renders with required correctly 1`] = `
"<div data-v-0fd934a4="" class="text-sm">
<div data-v-0fd934a4="" class="">
<div data-v-0fd934a4="" class="flex content-center items-center justify-between"><label data-v-0fd934a4="" for="2" class="block font-medium text-gray-700 dark:text-gray-200 after:content-['*'] after:ms-0.5 after:text-red-500 dark:after:text-red-400">Username</label>
<!--v-if-->
</div>
<!--v-if-->
</div>
<div data-v-0fd934a4="" class="mt-1 relative">
<transition-stub data-v-0fd934a4="" name="slide-fade" mode="out-in" appear="false" persisted="false" css="true">
<!--v-if-->
</transition-stub>
</div>
</div>"
`;
exports[`FormField > renders with size correctly 1`] = `
"<div data-v-0fd934a4="" class="text-base">
<div data-v-0fd934a4="" class="">
<div data-v-0fd934a4="" class="flex content-center items-center justify-between"><label data-v-0fd934a4="" for="1" class="block font-medium text-gray-700 dark:text-gray-200">Username</label>
<!--v-if-->
</div>
<p data-v-0fd934a4="" class="text-gray-500 dark:text-gray-400">Enter your username</p>
</div>
<div data-v-0fd934a4="" class="mt-1 relative">
<transition-stub data-v-0fd934a4="" name="slide-fade" mode="out-in" appear="false" persisted="false" css="true">
<!--v-if-->
</transition-stub>
</div>
</div>"
`;
exports[`FormField > renders with ui correctly 1`] = `
"<div data-v-0fd934a4="" class="text-sm">
<div data-v-0fd934a4="" class="">
<!--v-if-->
<!--v-if-->
</div>
<div data-v-0fd934a4="" class="">
<transition-stub data-v-0fd934a4="" name="slide-fade" mode="out-in" appear="false" persisted="false" css="true">
<!--v-if-->
</transition-stub>
</div>
</div>"
`;

View File

@@ -0,0 +1,199 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`Input > renders basic case correctly 1`] = `
"<div class="relative"><input type="text" class="relative block w-full disabled:cursor-not-allowed disabled:opacity-75 focus:outline-none border-0 rounded-md placeholder-gray-400 dark:placeholder-gray-500 text-sm gap-x-1.5 px-2.5 py-1.5 shadow-sm bg-white dark:bg-gray-900 text-gray-900 dark:text-white ring-1 ring-inset ring-gray-300 dark:ring-gray-700 focus:ring-2 focus:ring-primary-500 dark:focus:ring-primary-400">
<!-- span
v-if="(isLeading && leadingIconName) || $slots.leading"
:class="leadingWrapperIconClass"
>
<slot name="leading" :disabled="disabled" :loading="loading">
<UIcon :name="leadingIconName" :class="leadingIconClass" />
</slot>
</span>
<span
v-if="(isTrailing && trailingIconName) || $slots.trailing"
:class="trailingWrapperIconClass"
>
<slot name="trailing" :disabled="disabled" :loading="loading">
<UIcon :name="trailingIconName" :class="trailingIconClass" />
</slot>
</span -->
</div>"
`;
exports[`Input > renders with color correctly 1`] = `
"<div class="relative"><input type="text" class="relative block w-full disabled:cursor-not-allowed disabled:opacity-75 focus:outline-none border-0 rounded-md placeholder-gray-400 dark:placeholder-gray-500 text-sm gap-x-1.5 px-2.5 py-1.5 shadow-sm bg-transparent text-gray-900 dark:text-white ring-1 ring-inset ring-red-500 dark:ring-red-400 focus:ring-2 focus:ring-red-500 dark:focus:ring-red-400">
<!-- span
v-if="(isLeading && leadingIconName) || $slots.leading"
:class="leadingWrapperIconClass"
>
<slot name="leading" :disabled="disabled" :loading="loading">
<UIcon :name="leadingIconName" :class="leadingIconClass" />
</slot>
</span>
<span
v-if="(isTrailing && trailingIconName) || $slots.trailing"
:class="trailingWrapperIconClass"
>
<slot name="trailing" :disabled="disabled" :loading="loading">
<UIcon :name="trailingIconName" :class="trailingIconClass" />
</slot>
</span -->
</div>"
`;
exports[`Input > renders with disabled correctly 1`] = `
"<div class="relative"><input type="text" class="relative block w-full disabled:cursor-not-allowed disabled:opacity-75 focus:outline-none border-0 rounded-md placeholder-gray-400 dark:placeholder-gray-500 text-sm gap-x-1.5 px-2.5 py-1.5 shadow-sm bg-white dark:bg-gray-900 text-gray-900 dark:text-white ring-1 ring-inset ring-gray-300 dark:ring-gray-700 focus:ring-2 focus:ring-primary-500 dark:focus:ring-primary-400" disabled="">
<!-- span
v-if="(isLeading && leadingIconName) || $slots.leading"
:class="leadingWrapperIconClass"
>
<slot name="leading" :disabled="disabled" :loading="loading">
<UIcon :name="leadingIconName" :class="leadingIconClass" />
</slot>
</span>
<span
v-if="(isTrailing && trailingIconName) || $slots.trailing"
:class="trailingWrapperIconClass"
>
<slot name="trailing" :disabled="disabled" :loading="loading">
<UIcon :name="trailingIconName" :class="trailingIconClass" />
</slot>
</span -->
</div>"
`;
exports[`Input > renders with name correctly 1`] = `
"<div class="relative"><input type="text" name="username" class="relative block w-full disabled:cursor-not-allowed disabled:opacity-75 focus:outline-none border-0 rounded-md placeholder-gray-400 dark:placeholder-gray-500 text-sm gap-x-1.5 px-2.5 py-1.5 shadow-sm bg-white dark:bg-gray-900 text-gray-900 dark:text-white ring-1 ring-inset ring-gray-300 dark:ring-gray-700 focus:ring-2 focus:ring-primary-500 dark:focus:ring-primary-400">
<!-- span
v-if="(isLeading && leadingIconName) || $slots.leading"
:class="leadingWrapperIconClass"
>
<slot name="leading" :disabled="disabled" :loading="loading">
<UIcon :name="leadingIconName" :class="leadingIconClass" />
</slot>
</span>
<span
v-if="(isTrailing && trailingIconName) || $slots.trailing"
:class="trailingWrapperIconClass"
>
<slot name="trailing" :disabled="disabled" :loading="loading">
<UIcon :name="trailingIconName" :class="trailingIconClass" />
</slot>
</span -->
</div>"
`;
exports[`Input > renders with placeholder correctly 1`] = `
"<div class="relative"><input type="text" placeholder="Enter your username" class="relative block w-full disabled:cursor-not-allowed disabled:opacity-75 focus:outline-none border-0 rounded-md placeholder-gray-400 dark:placeholder-gray-500 text-sm gap-x-1.5 px-2.5 py-1.5 shadow-sm bg-white dark:bg-gray-900 text-gray-900 dark:text-white ring-1 ring-inset ring-gray-300 dark:ring-gray-700 focus:ring-2 focus:ring-primary-500 dark:focus:ring-primary-400">
<!-- span
v-if="(isLeading && leadingIconName) || $slots.leading"
:class="leadingWrapperIconClass"
>
<slot name="leading" :disabled="disabled" :loading="loading">
<UIcon :name="leadingIconName" :class="leadingIconClass" />
</slot>
</span>
<span
v-if="(isTrailing && trailingIconName) || $slots.trailing"
:class="trailingWrapperIconClass"
>
<slot name="trailing" :disabled="disabled" :loading="loading">
<UIcon :name="trailingIconName" :class="trailingIconClass" />
</slot>
</span -->
</div>"
`;
exports[`Input > renders with required correctly 1`] = `
"<div class="relative"><input type="text" class="relative block w-full disabled:cursor-not-allowed disabled:opacity-75 focus:outline-none border-0 rounded-md placeholder-gray-400 dark:placeholder-gray-500 text-sm gap-x-1.5 px-2.5 py-1.5 shadow-sm bg-white dark:bg-gray-900 text-gray-900 dark:text-white ring-1 ring-inset ring-gray-300 dark:ring-gray-700 focus:ring-2 focus:ring-primary-500 dark:focus:ring-primary-400">
<!-- span
v-if="(isLeading && leadingIconName) || $slots.leading"
:class="leadingWrapperIconClass"
>
<slot name="leading" :disabled="disabled" :loading="loading">
<UIcon :name="leadingIconName" :class="leadingIconClass" />
</slot>
</span>
<span
v-if="(isTrailing && trailingIconName) || $slots.trailing"
:class="trailingWrapperIconClass"
>
<slot name="trailing" :disabled="disabled" :loading="loading">
<UIcon :name="trailingIconName" :class="trailingIconClass" />
</slot>
</span -->
</div>"
`;
exports[`Input > renders with size correctly 1`] = `
"<div class="relative"><input type="text" class="relative block w-full disabled:cursor-not-allowed disabled:opacity-75 focus:outline-none border-0 rounded-md placeholder-gray-400 dark:placeholder-gray-500 text-sm gap-x-1.5 px-2.5 py-1.5 shadow-sm bg-white dark:bg-gray-900 text-gray-900 dark:text-white ring-1 ring-inset ring-gray-300 dark:ring-gray-700 focus:ring-2 focus:ring-primary-500 dark:focus:ring-primary-400">
<!-- span
v-if="(isLeading && leadingIconName) || $slots.leading"
:class="leadingWrapperIconClass"
>
<slot name="leading" :disabled="disabled" :loading="loading">
<UIcon :name="leadingIconName" :class="leadingIconClass" />
</slot>
</span>
<span
v-if="(isTrailing && trailingIconName) || $slots.trailing"
:class="trailingWrapperIconClass"
>
<slot name="trailing" :disabled="disabled" :loading="loading">
<UIcon :name="trailingIconName" :class="trailingIconClass" />
</slot>
</span -->
</div>"
`;
exports[`Input > renders with type correctly 1`] = `
"<div class="relative"><input type="password" class="relative block w-full disabled:cursor-not-allowed disabled:opacity-75 focus:outline-none border-0 rounded-md placeholder-gray-400 dark:placeholder-gray-500 text-sm gap-x-1.5 px-2.5 py-1.5 shadow-sm bg-white dark:bg-gray-900 text-gray-900 dark:text-white ring-1 ring-inset ring-gray-300 dark:ring-gray-700 focus:ring-2 focus:ring-primary-500 dark:focus:ring-primary-400">
<!-- span
v-if="(isLeading && leadingIconName) || $slots.leading"
:class="leadingWrapperIconClass"
>
<slot name="leading" :disabled="disabled" :loading="loading">
<UIcon :name="leadingIconName" :class="leadingIconClass" />
</slot>
</span>
<span
v-if="(isTrailing && trailingIconName) || $slots.trailing"
:class="trailingWrapperIconClass"
>
<slot name="trailing" :disabled="disabled" :loading="loading">
<UIcon :name="trailingIconName" :class="trailingIconClass" />
</slot>
</span -->
</div>"
`;
exports[`Input > renders with variant correctly 1`] = `
"<div class="relative"><input type="text" class="relative block w-full disabled:cursor-not-allowed disabled:opacity-75 focus:outline-none border-0 rounded-md placeholder-gray-400 dark:placeholder-gray-500 text-sm gap-x-1.5 px-2.5 py-1.5 shadow-sm bg-white dark:bg-gray-900 text-gray-900 dark:text-white ring-1 ring-inset ring-gray-300 dark:ring-gray-700 focus:ring-2 focus:ring-primary-500 dark:focus:ring-primary-400">
<!-- span
v-if="(isLeading && leadingIconName) || $slots.leading"
:class="leadingWrapperIconClass"
>
<slot name="leading" :disabled="disabled" :loading="loading">
<UIcon :name="leadingIconName" :class="leadingIconClass" />
</slot>
</span>
<span
v-if="(isTrailing && trailingIconName) || $slots.trailing"
:class="trailingWrapperIconClass"
>
<slot name="trailing" :disabled="disabled" :loading="loading">
<UIcon :name="trailingIconName" :class="trailingIconClass" />
</slot>
</span -->
</div>"
`;