mirror of
https://github.com/ArthurDanjou/ui.git
synced 2026-01-20 23:11:43 +01:00
feat(Form): new component (#4)
Co-authored-by: Benjamin Canac <canacb1@gmail.com>
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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:*"
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -2,6 +2,6 @@
|
||||
export default defineNuxtConfig({
|
||||
modules: ['../src/module'],
|
||||
ui: {
|
||||
colors: ['primary']
|
||||
colors: ['primary', 'red']
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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>
|
||||
|
||||
35
playground/pages/form-field.vue
Normal file
35
playground/pages/form-field.vue
Normal 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
104
playground/pages/form.vue
Normal 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>
|
||||
19
playground/pages/input.vue
Normal file
19
playground/pages/input.vue
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
80
pnpm-lock.yaml
generated
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
196
src/runtime/components/Form.vue
Normal file
196
src/runtime/components/Form.vue
Normal 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>
|
||||
132
src/runtime/components/FormField.vue
Normal file
132
src/runtime/components/FormField.vue
Normal 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>
|
||||
176
src/runtime/components/Input.vue
Normal file
176
src/runtime/components/Input.vue
Normal 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>
|
||||
@@ -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)
|
||||
|
||||
|
||||
70
src/runtime/composables/useFormField.ts
Normal file
70
src/runtime/composables/useFormField.ts
Normal 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
51
src/runtime/types/form.d.ts
vendored
Normal 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>
|
||||
}
|
||||
1
src/runtime/types/index.d.ts
vendored
1
src/runtime/types/index.d.ts
vendored
@@ -0,0 +1 @@
|
||||
|
||||
|
||||
83
src/runtime/utils/form.ts
Normal file
83
src/runtime/utils/form.ts
Normal 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 []
|
||||
}
|
||||
4
src/runtime/utils/index.ts
Normal file
4
src/runtime/utils/index.ts
Normal 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
3
src/theme/form.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export default {
|
||||
base: ''
|
||||
}
|
||||
32
src/theme/formField.ts
Normal file
32
src/theme/formField.ts
Normal 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'
|
||||
}
|
||||
}
|
||||
@@ -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
66
src/theme/input.ts
Normal 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'
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
450
test/components/Form.spec.ts
Normal file
450
test/components/Form.spec.ts
Normal 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')
|
||||
})
|
||||
})
|
||||
35
test/components/FormField.spec.ts
Normal file
35
test/components/FormField.spec.ts
Normal 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()
|
||||
})
|
||||
})
|
||||
28
test/components/Input.spec.ts
Normal file
28
test/components/Input.spec.ts
Normal 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()
|
||||
})
|
||||
})
|
||||
@@ -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()
|
||||
})
|
||||
|
||||
655
test/components/__snapshots__/Form.spec.ts.snap
Normal file
655
test/components/__snapshots__/Form.spec.ts.snap
Normal 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>"
|
||||
`;
|
||||
202
test/components/__snapshots__/FormField.spec.ts.snap
Normal file
202
test/components/__snapshots__/FormField.spec.ts.snap
Normal 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>"
|
||||
`;
|
||||
199
test/components/__snapshots__/Input.spec.ts.snap
Normal file
199
test/components/__snapshots__/Input.spec.ts.snap
Normal 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>"
|
||||
`;
|
||||
Reference in New Issue
Block a user