@zeno-lib/forms 0.9.1 → 0.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/create-zeno-form.d.mts +1 -2
- package/dist/create-zeno-form.mjs +1 -2
- package/dist/form-element.d.mts +0 -2
- package/dist/form-element.mjs +1 -2
- package/dist/lib/action-result.d.mts +0 -2
- package/dist/lib/action-result.mjs +1 -2
- package/dist/lib/apply-validation-error.d.mts +0 -2
- package/dist/lib/apply-validation-error.mjs +1 -2
- package/dist/lib/aria.d.mts +1 -3
- package/dist/lib/aria.mjs +1 -2
- package/dist/lib/contexts.d.mts +0 -2
- package/dist/lib/contexts.mjs +1 -2
- package/dist/lib/date-value.d.mts +0 -2
- package/dist/lib/date-value.mjs +2 -3
- package/dist/lib/default-props.d.mts +1 -3
- package/dist/lib/default-props.mjs +1 -2
- package/dist/lib/empty-value.d.mts +9 -3
- package/dist/lib/empty-value.mjs +15 -5
- package/dist/lib/field-hints.d.mts +16 -0
- package/dist/lib/field-hints.mjs +23 -0
- package/dist/lib/focus-first-invalid.d.mts +0 -2
- package/dist/lib/focus-first-invalid.mjs +1 -2
- package/dist/lib/form-disabled.d.mts +0 -2
- package/dist/lib/form-disabled.mjs +1 -2
- package/dist/lib/formatted-number.d.mts +0 -2
- package/dist/lib/formatted-number.mjs +10 -11
- package/dist/lib/item-options.d.mts +0 -2
- package/dist/lib/item-options.mjs +1 -2
- package/dist/lib/schema-defaults.d.mts +6 -3
- package/dist/lib/schema-defaults.mjs +37 -5
- package/dist/lib/schema-required.d.mts +0 -2
- package/dist/lib/schema-required.mjs +3 -4
- package/dist/lib/submit-action.d.mts +0 -2
- package/dist/lib/submit-action.mjs +1 -2
- package/dist/lib/submit-error.d.mts +0 -2
- package/dist/lib/submit-error.mjs +1 -2
- package/dist/lib/use-field-control.d.mts +0 -2
- package/dist/lib/use-field-control.mjs +1 -2
- package/dist/lib/use-form-dialog.d.mts +0 -2
- package/dist/lib/use-form-dialog.mjs +2 -3
- package/dist/lib/use-formatted-number.d.mts +0 -2
- package/dist/lib/use-formatted-number.mjs +1 -2
- package/dist/lib/use-is-invalid.d.mts +0 -2
- package/dist/lib/use-is-invalid.mjs +1 -2
- package/dist/lib/use-rebased-default-values.d.mts +0 -2
- package/dist/lib/use-rebased-default-values.mjs +1 -2
- package/dist/lib/use-unsaved-changes-warning.d.mts +0 -2
- package/dist/lib/use-unsaved-changes-warning.mjs +1 -2
- package/dist/lib/validation-error.d.mts +0 -2
- package/dist/lib/validation-error.mjs +1 -2
- package/dist/lib/validation-logic.d.mts +0 -2
- package/dist/lib/validation-logic.mjs +1 -2
- package/dist/lib/validation-modes.d.mts +0 -2
- package/dist/lib/validation-modes.mjs +1 -2
- package/package.json +8 -8
- package/src/composition.test.tsx +3 -3
- package/src/control-field.test.tsx +1 -1
- package/src/date-picker-field.test.tsx +3 -3
- package/src/field-hints.test.tsx +118 -0
- package/src/fields/input-field.tsx +6 -4
- package/src/fields/percentage-field.tsx +16 -8
- package/src/fields/textarea-field.tsx +6 -4
- package/src/fields/year-field.tsx +8 -3
- package/src/form-dialog.test.tsx +3 -3
- package/src/form-error.test.tsx +1 -1
- package/src/formatted-number-fields.test.tsx +3 -3
- package/src/lib/date-value.ts +2 -1
- package/src/lib/empty-value.ts +26 -3
- package/src/lib/field-hints.ts +64 -0
- package/src/lib/formatted-number.ts +9 -9
- package/src/lib/schema-defaults.test.ts +6 -3
- package/src/lib/schema-defaults.ts +60 -3
- package/src/lib/schema-required.ts +2 -2
- package/src/lib/use-form-dialog.ts +1 -1
- package/src/multi-value-fields.test.tsx +2 -2
- package/src/nullable-text.test.tsx +80 -0
- package/src/submit-action.test.tsx +4 -4
- package/src/use-app-fields.test.tsx +10 -10
- package/src/use-form.cascade.test.tsx +2 -2
- package/src/use-form.reset-defaults.test.tsx +2 -2
- package/src/use-form.test.tsx +3 -3
- package/src/use-form.unsaved-warning.test.tsx +1 -1
- package/src/use-form.validation-modes.test.tsx +2 -2
- package/src/with-form.test.tsx +1 -1
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { AnyFieldApi } from "@tanstack/react-form"
|
|
2
|
+
import { toPathKeys } from "./empty-value"
|
|
3
|
+
import { findZodSchemaAt } from "./schema-defaults"
|
|
4
|
+
import { getFormSchema } from "./validation-modes"
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* What a field reads from the column its schema comes from, when the column's
|
|
8
|
+
* hint names it. The `percentage()` and `year()` columns of
|
|
9
|
+
* `@zeno-lib/db/schema` give one, so `PercentageField` and `YearField` show
|
|
10
|
+
* and clamp what the column accepts with no props: the scale from the hint,
|
|
11
|
+
* the bounds from the schema's own `.min()` and `.max()`, a caller's
|
|
12
|
+
* narrowing included. A prop the field is given wins.
|
|
13
|
+
*/
|
|
14
|
+
type FieldHints = {
|
|
15
|
+
readonly max?: number | undefined
|
|
16
|
+
readonly min?: number | undefined
|
|
17
|
+
readonly scale?: "fraction" | "percent" | undefined
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// The registry `@zeno-lib/schema` puts each column's hint in, under a
|
|
21
|
+
// `globalThis` symbol both packages name, so this one reads it without
|
|
22
|
+
// importing that one. Absent until `@zeno-lib/schema` loads, and then no
|
|
23
|
+
// field has a hint.
|
|
24
|
+
const FIELD_HINTS_KEY = Symbol.for("@zeno-lib/field-hints")
|
|
25
|
+
|
|
26
|
+
type HintRegistry = { readonly get: (schema: object) => unknown }
|
|
27
|
+
|
|
28
|
+
type Bounded = { readonly maxValue?: unknown; readonly minValue?: unknown }
|
|
29
|
+
|
|
30
|
+
const readHint = (node: object) => {
|
|
31
|
+
const registry = (globalThis as { [FIELD_HINTS_KEY]?: HintRegistry })[
|
|
32
|
+
FIELD_HINTS_KEY
|
|
33
|
+
]
|
|
34
|
+
const hint = registry?.get(node) as { field?: unknown } | undefined
|
|
35
|
+
return typeof hint?.field === "string" ? hint : undefined
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// A schema with no `.min()` or `.max()` reports `null` or an infinity.
|
|
39
|
+
const toBound = (value: unknown) =>
|
|
40
|
+
typeof value === "number" && Number.isFinite(value) ? value : undefined
|
|
41
|
+
|
|
42
|
+
function getFieldHints(field: AnyFieldApi, kind: string): FieldHints {
|
|
43
|
+
const schema = getFormSchema(field.form)
|
|
44
|
+
const found = schema
|
|
45
|
+
? findZodSchemaAt(
|
|
46
|
+
schema as Parameters<typeof findZodSchemaAt>[0],
|
|
47
|
+
toPathKeys(field.name),
|
|
48
|
+
readHint
|
|
49
|
+
)
|
|
50
|
+
: undefined
|
|
51
|
+
if (found?.value.field !== kind) {
|
|
52
|
+
return {}
|
|
53
|
+
}
|
|
54
|
+
const { scale } = found.value as { scale?: unknown }
|
|
55
|
+
const { maxValue, minValue } = found.node as Bounded
|
|
56
|
+
return {
|
|
57
|
+
max: toBound(maxValue),
|
|
58
|
+
min: toBound(minValue),
|
|
59
|
+
scale: scale === "fraction" || scale === "percent" ? scale : undefined,
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export type { FieldHints }
|
|
64
|
+
export { getFieldHints }
|
|
@@ -43,15 +43,15 @@ const separatorCache = new Map<string, NumberSeparators>()
|
|
|
43
43
|
|
|
44
44
|
// Minus signs a user may type or paste: ASCII hyphen-minus, U+2212 (what Intl
|
|
45
45
|
// emits for some locales), and the en dash.
|
|
46
|
-
const MINUS_SIGNS = /[-−–]/
|
|
47
|
-
const LEADING_MINUS = /^[^\d]*[-−–]/
|
|
48
|
-
const NON_DIGIT = /\D/
|
|
49
|
-
const LEADING_ZEROS = /^0+(?=\d)/
|
|
50
|
-
const DIGIT = /\d/
|
|
51
|
-
const DIGITS = /\d/
|
|
52
|
-
const PARENTHESIZED = /^\s*\(.*\)\s*$/
|
|
53
|
-
const NOT_NUMERIC = /[^\d.,]/
|
|
54
|
-
const UNICODE_MINUS = /\u2212/
|
|
46
|
+
const MINUS_SIGNS = /[-−–]/u
|
|
47
|
+
const LEADING_MINUS = /^[^\d]*[-−–]/u
|
|
48
|
+
const NON_DIGIT = /\D/gu
|
|
49
|
+
const LEADING_ZEROS = /^0+(?=\d)/u
|
|
50
|
+
const DIGIT = /\d/u
|
|
51
|
+
const DIGITS = /\d/gu
|
|
52
|
+
const PARENTHESIZED = /^\s*\(.*\)\s*$/u
|
|
53
|
+
const NOT_NUMERIC = /[^\d.,]/gu
|
|
54
|
+
const UNICODE_MINUS = /\u2212/gu
|
|
55
55
|
|
|
56
56
|
function getNumberSeparators(locale?: string): NumberSeparators {
|
|
57
57
|
const key = locale ?? ""
|
|
@@ -65,9 +65,12 @@ describe("extractZodDefaults — wrappers", () => {
|
|
|
65
65
|
expect(extractZodDefaults(schema)).toEqual({ x: "" })
|
|
66
66
|
})
|
|
67
67
|
|
|
68
|
-
test(".nullable()
|
|
69
|
-
const schema = z.object({
|
|
70
|
-
|
|
68
|
+
test(".nullable() starts a string at null, which a nullable column stores", () => {
|
|
69
|
+
const schema = z.object({
|
|
70
|
+
x: z.string().nullable(),
|
|
71
|
+
y: z.string().nullable().optional(),
|
|
72
|
+
})
|
|
73
|
+
expect(extractZodDefaults(schema)).toEqual({ x: null, y: null })
|
|
71
74
|
})
|
|
72
75
|
|
|
73
76
|
test(".readonly() unwraps to inner string → ''", () => {
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
// Tier A+ semantics:
|
|
5
5
|
// - `.default(...)` / `.prefault(...)` values flow through.
|
|
6
6
|
// - Strings (and string-format subtypes — email, url, uuid, regex, …) without
|
|
7
|
-
// a default fall back to `""` so React inputs render controlled
|
|
7
|
+
// a default fall back to `""` so React inputs render controlled, or to
|
|
8
|
+
// `null` under `.nullable()`, which the fields render as no text.
|
|
8
9
|
// - Arrays without a default fall back to `[]`.
|
|
9
10
|
// - Nested `z.object(...)` recurses; emits a sub-record only if at least one
|
|
10
11
|
// leaf produced a value.
|
|
@@ -26,6 +27,7 @@ type SchemaDef = {
|
|
|
26
27
|
readonly shape?: Record<string, SchemaNode>
|
|
27
28
|
readonly catchall?: SchemaNode
|
|
28
29
|
readonly in?: SchemaNode
|
|
30
|
+
readonly out?: SchemaNode
|
|
29
31
|
readonly element?: SchemaNode
|
|
30
32
|
readonly defaultValue?: unknown
|
|
31
33
|
}
|
|
@@ -40,6 +42,7 @@ function getDef(node: SchemaNode | undefined): SchemaDef | undefined {
|
|
|
40
42
|
|
|
41
43
|
function extractDefaultForField(node: SchemaNode): ExtractResult {
|
|
42
44
|
let current: SchemaNode | undefined = node
|
|
45
|
+
let isNullable = false
|
|
43
46
|
for (let depth = 0; depth < MAX_WRAPPER_DEPTH; depth += 1) {
|
|
44
47
|
const def: SchemaDef | undefined = getDef(current)
|
|
45
48
|
if (!def?.type) {
|
|
@@ -49,8 +52,11 @@ function extractDefaultForField(node: SchemaNode): ExtractResult {
|
|
|
49
52
|
case "default":
|
|
50
53
|
case "prefault":
|
|
51
54
|
return { ok: true, value: def.defaultValue }
|
|
52
|
-
case "optional":
|
|
53
55
|
case "nullable":
|
|
56
|
+
isNullable = true
|
|
57
|
+
current = def.innerType
|
|
58
|
+
continue
|
|
59
|
+
case "optional":
|
|
54
60
|
case "nonoptional":
|
|
55
61
|
case "readonly":
|
|
56
62
|
current = def.innerType
|
|
@@ -58,8 +64,10 @@ function extractDefaultForField(node: SchemaNode): ExtractResult {
|
|
|
58
64
|
case "pipe":
|
|
59
65
|
current = def.in
|
|
60
66
|
continue
|
|
67
|
+
// No text is `null` where the schema takes it, as a nullable column
|
|
68
|
+
// stores it, so a field left blank saves as none rather than as `""`.
|
|
61
69
|
case "string":
|
|
62
|
-
return { ok: true, value: "" }
|
|
70
|
+
return { ok: true, value: isNullable ? null : "" }
|
|
63
71
|
case "array":
|
|
64
72
|
return { ok: true, value: [] }
|
|
65
73
|
case "object": {
|
|
@@ -142,6 +150,54 @@ function extractZodDefaultAt(
|
|
|
142
150
|
}
|
|
143
151
|
}
|
|
144
152
|
|
|
153
|
+
// The schema inside a wrapper, or `undefined` past the last one.
|
|
154
|
+
function unwrapped(node: SchemaNode): SchemaNode | undefined {
|
|
155
|
+
const def: SchemaDef | undefined = getDef(node)
|
|
156
|
+
switch (def?.type) {
|
|
157
|
+
case "catch":
|
|
158
|
+
case "default":
|
|
159
|
+
case "nonoptional":
|
|
160
|
+
case "nullable":
|
|
161
|
+
case "optional":
|
|
162
|
+
case "prefault":
|
|
163
|
+
case "readonly":
|
|
164
|
+
return def.innerType
|
|
165
|
+
// `z.preprocess` pipes a transform into the schema it feeds.
|
|
166
|
+
case "pipe":
|
|
167
|
+
return getDef(def.in)?.type === "transform" ? def.out : def.in
|
|
168
|
+
default:
|
|
169
|
+
return undefined
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// The schema at `keys`, or inside one of its wrappers, that `read` finds a
|
|
174
|
+
// value on, with that value. A Zod registry keeps an entry on the schema it
|
|
175
|
+
// was set on, not on the `.optional()` or `.nullable()` around it.
|
|
176
|
+
function findZodSchemaAt<T>(
|
|
177
|
+
schema: SchemaNode,
|
|
178
|
+
keys: readonly PropertyKey[],
|
|
179
|
+
read: (node: SchemaNode) => T | undefined
|
|
180
|
+
): { node: SchemaNode; value: T } | undefined {
|
|
181
|
+
try {
|
|
182
|
+
let node: SchemaNode | null = schema
|
|
183
|
+
for (const key of keys) {
|
|
184
|
+
node = node && childAt(node, key)
|
|
185
|
+
}
|
|
186
|
+
let current: SchemaNode | undefined =
|
|
187
|
+
node && keys.length > 0 ? node : undefined
|
|
188
|
+
for (let depth = 0; current && depth < MAX_WRAPPER_DEPTH; depth += 1) {
|
|
189
|
+
const value = read(current)
|
|
190
|
+
if (value !== undefined) {
|
|
191
|
+
return { node: current, value }
|
|
192
|
+
}
|
|
193
|
+
current = unwrapped(current)
|
|
194
|
+
}
|
|
195
|
+
} catch {
|
|
196
|
+
// An unrecognised schema has nothing to offer.
|
|
197
|
+
}
|
|
198
|
+
return undefined
|
|
199
|
+
}
|
|
200
|
+
|
|
145
201
|
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
146
202
|
return (
|
|
147
203
|
typeof value === "object" &&
|
|
@@ -244,5 +300,6 @@ export {
|
|
|
244
300
|
deepMergeDefaults,
|
|
245
301
|
extractZodDefaultAt,
|
|
246
302
|
extractZodDefaults,
|
|
303
|
+
findZodSchemaAt,
|
|
247
304
|
pickSchemaShape,
|
|
248
305
|
}
|
|
@@ -53,10 +53,10 @@ type StandardSchemaLike = {
|
|
|
53
53
|
// Each level can take two passes: the empty value, then the descent.
|
|
54
54
|
const MAX_PROBE_DEPTH = 16
|
|
55
55
|
|
|
56
|
-
const INDEX_SEGMENT = /^\d+$/
|
|
56
|
+
const INDEX_SEGMENT = /^\d+$/u
|
|
57
57
|
|
|
58
58
|
// Matches `[3]` and `.3` index segments in a field name.
|
|
59
|
-
const INDEX_IN_NAME = /\[\d+\]
|
|
59
|
+
const INDEX_IN_NAME = /\[\d+\]|\.\d+(?=\.|\[|$)/gu
|
|
60
60
|
|
|
61
61
|
function pathKey(part: PathPart): PropertyKey {
|
|
62
62
|
// biome-ignore lint/suspicious/noUnnecessaryConditions: issue paths come from third-party validators at runtime, and a non-conforming `null` segment would make `in` throw.
|
|
@@ -199,7 +199,7 @@ const FOCUSABLE =
|
|
|
199
199
|
'input:not([type="hidden"]):not([disabled]), textarea:not([disabled]), select:not([disabled]), button:not([disabled]), [tabindex]:not([tabindex="-1"])'
|
|
200
200
|
|
|
201
201
|
function attributeSelector(attribute: string, value: string): string {
|
|
202
|
-
return `[${attribute}="${value.replace(/["\\]/
|
|
202
|
+
return `[${attribute}="${value.replace(/["\\]/gu, "\\$&")}"]`
|
|
203
203
|
}
|
|
204
204
|
|
|
205
205
|
/**
|
|
@@ -3,8 +3,8 @@ import userEvent from "@zeno-lib/test/user-event"
|
|
|
3
3
|
import { afterEach, describe, expect, test, vi } from "vitest"
|
|
4
4
|
import { Form, FormProvider, useForm } from "./create-form"
|
|
5
5
|
|
|
6
|
-
const TAGS = /Tags/
|
|
7
|
-
const CLEAR = /Clear/
|
|
6
|
+
const TAGS = /Tags/u
|
|
7
|
+
const CLEAR = /Clear/u
|
|
8
8
|
|
|
9
9
|
afterEach(() => {
|
|
10
10
|
cleanup()
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { cleanup, render, screen } from "@zeno-lib/test/testing-library"
|
|
2
|
+
import userEvent from "@zeno-lib/test/user-event"
|
|
3
|
+
import { afterEach, expect, test, vi } from "vitest"
|
|
4
|
+
import { z } from "zod"
|
|
5
|
+
import { Form, FormProvider, useForm } from "./create-form"
|
|
6
|
+
|
|
7
|
+
afterEach(cleanup)
|
|
8
|
+
|
|
9
|
+
test("a nullable text field holds null for no text, from the start and once emptied", async () => {
|
|
10
|
+
const user = userEvent.setup()
|
|
11
|
+
const schema = z.object({
|
|
12
|
+
note: z.string().nullable(),
|
|
13
|
+
optional: z.string().optional(),
|
|
14
|
+
title: z.string(),
|
|
15
|
+
})
|
|
16
|
+
const captured: { values?: unknown } = {}
|
|
17
|
+
function H() {
|
|
18
|
+
const form = useForm({ onSubmit: vi.fn(), schema })
|
|
19
|
+
const { InputField, Subscribe, TextAreaField } = form
|
|
20
|
+
return (
|
|
21
|
+
<FormProvider form={form}>
|
|
22
|
+
<Form>
|
|
23
|
+
<TextAreaField label="Note" name="note" />
|
|
24
|
+
<InputField label="Optional" name="optional" />
|
|
25
|
+
<InputField label="Title" name="title" />
|
|
26
|
+
<Subscribe selector={(state) => state.values}>
|
|
27
|
+
{(values) => {
|
|
28
|
+
captured.values = values
|
|
29
|
+
return null
|
|
30
|
+
}}
|
|
31
|
+
</Subscribe>
|
|
32
|
+
</Form>
|
|
33
|
+
</FormProvider>
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
render(<H />)
|
|
37
|
+
// An optional field keeps `""`: an update skips an `undefined` column, so
|
|
38
|
+
// emptying it would keep the stored text.
|
|
39
|
+
const empty = { note: null, optional: "", title: "" }
|
|
40
|
+
expect(captured.values).toStrictEqual(empty)
|
|
41
|
+
|
|
42
|
+
for (const label of ["Note", "Optional", "Title"]) {
|
|
43
|
+
// biome-ignore lint/performance/noAwaitInLoops: one field after another, as a user types
|
|
44
|
+
await user.type(screen.getByLabelText(label), "x")
|
|
45
|
+
await user.clear(screen.getByLabelText(label))
|
|
46
|
+
}
|
|
47
|
+
expect(captured.values).toStrictEqual(empty)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
test("an emptied nullable text field holds null whatever the other fields hold", async () => {
|
|
51
|
+
const user = userEvent.setup()
|
|
52
|
+
// Once every field passes, the async refinement keeps the form from being
|
|
53
|
+
// checked synchronously.
|
|
54
|
+
const schema = z
|
|
55
|
+
.object({ note: z.string().nullable() })
|
|
56
|
+
.refine(async () => await Promise.resolve(true))
|
|
57
|
+
const captured: { values?: unknown } = {}
|
|
58
|
+
function H() {
|
|
59
|
+
const form = useForm({ onSubmit: vi.fn(), schema })
|
|
60
|
+
const { InputField, Subscribe } = form
|
|
61
|
+
return (
|
|
62
|
+
<FormProvider form={form}>
|
|
63
|
+
<Form>
|
|
64
|
+
<InputField label="Note" name="note" />
|
|
65
|
+
<Subscribe selector={(state) => state.values}>
|
|
66
|
+
{(values) => {
|
|
67
|
+
captured.values = values
|
|
68
|
+
return null
|
|
69
|
+
}}
|
|
70
|
+
</Subscribe>
|
|
71
|
+
</Form>
|
|
72
|
+
</FormProvider>
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
render(<H />)
|
|
76
|
+
|
|
77
|
+
await user.type(screen.getByRole("textbox"), "x")
|
|
78
|
+
await user.clear(screen.getByRole("textbox"))
|
|
79
|
+
expect(captured.values).toStrictEqual({ note: null })
|
|
80
|
+
})
|
|
@@ -14,10 +14,10 @@ afterEach(() => {
|
|
|
14
14
|
})
|
|
15
15
|
|
|
16
16
|
const SUBMIT = { name: "Submit" }
|
|
17
|
-
const EMAIL_LABEL = /Email/
|
|
18
|
-
const NAME_LABEL = /Name/
|
|
19
|
-
const OWNER_1_SHARE_LABEL = /^Owner 1 share/
|
|
20
|
-
const OWNER_2_SHARE_LABEL = /^Owner 2 share/
|
|
17
|
+
const EMAIL_LABEL = /Email/u
|
|
18
|
+
const NAME_LABEL = /Name/u
|
|
19
|
+
const OWNER_1_SHARE_LABEL = /^Owner 1 share/u
|
|
20
|
+
const OWNER_2_SHARE_LABEL = /^Owner 2 share/u
|
|
21
21
|
|
|
22
22
|
function failure(
|
|
23
23
|
fieldErrors: Record<string, string[]>,
|
|
@@ -12,16 +12,16 @@ import { z } from "zod"
|
|
|
12
12
|
import { SelectItem } from "@/components/ui/select"
|
|
13
13
|
import { Form, FormProvider, RadioGroupFieldItem, useForm } from "./create-form"
|
|
14
14
|
|
|
15
|
-
const CONTACT_EMAIL_LABEL = /Contact email/
|
|
16
|
-
const PASSWORD_LABEL = /Password/
|
|
17
|
-
const NAME_LABEL = /Name/
|
|
18
|
-
const ACCEPT_NAME = /Accept/
|
|
19
|
-
const TITLE_LABEL = /Title/
|
|
20
|
-
const NOTE_LABEL = /Note/
|
|
21
|
-
const DAY_LABEL = /Day/
|
|
22
|
-
const PICKED_DAY = /October 9/
|
|
23
|
-
const ROLE_LABEL = /Role/
|
|
24
|
-
const OWNER_LABEL = /Owner/
|
|
15
|
+
const CONTACT_EMAIL_LABEL = /Contact email/u
|
|
16
|
+
const PASSWORD_LABEL = /Password/u
|
|
17
|
+
const NAME_LABEL = /Name/u
|
|
18
|
+
const ACCEPT_NAME = /Accept/u
|
|
19
|
+
const TITLE_LABEL = /Title/u
|
|
20
|
+
const NOTE_LABEL = /Note/u
|
|
21
|
+
const DAY_LABEL = /Day/u
|
|
22
|
+
const PICKED_DAY = /October 9/u
|
|
23
|
+
const ROLE_LABEL = /Role/u
|
|
24
|
+
const OWNER_LABEL = /Owner/u
|
|
25
25
|
const PICK_A_ROLE = "Pick a role"
|
|
26
26
|
const PICK_A_DAY = "Pick a day"
|
|
27
27
|
|
|
@@ -4,8 +4,8 @@ import { afterEach, describe, expect, test, vi } from "vitest"
|
|
|
4
4
|
import { z } from "zod"
|
|
5
5
|
import { Form, FormProvider, useForm } from "./create-form"
|
|
6
6
|
|
|
7
|
-
const DIET_LABEL = /Diet/
|
|
8
|
-
const SAUSAGE_LABEL = /Sausage/
|
|
7
|
+
const DIET_LABEL = /Diet/u
|
|
8
|
+
const SAUSAGE_LABEL = /Sausage/u
|
|
9
9
|
|
|
10
10
|
afterEach(() => {
|
|
11
11
|
cleanup()
|
package/src/use-form.test.tsx
CHANGED
|
@@ -6,9 +6,9 @@ import { z } from "zod"
|
|
|
6
6
|
import { Form, FormProvider, useForm } from "./create-form"
|
|
7
7
|
import { ValidationError } from "./lib/validation-error"
|
|
8
8
|
|
|
9
|
-
const EMAIL_LABEL = /Email/
|
|
10
|
-
const VALID_EMAIL_MSG = /Enter a valid email/
|
|
11
|
-
const SUBMIT_NAME = /Submit/
|
|
9
|
+
const EMAIL_LABEL = /Email/u
|
|
10
|
+
const VALID_EMAIL_MSG = /Enter a valid email/iu
|
|
11
|
+
const SUBMIT_NAME = /Submit/u
|
|
12
12
|
|
|
13
13
|
// @testing-library auto-cleanup only runs with `globals: true`. Our vitest
|
|
14
14
|
// config keeps globals off (we import test helpers explicitly), so add the
|
|
@@ -4,7 +4,7 @@ import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"
|
|
|
4
4
|
import { z } from "zod"
|
|
5
5
|
import { Form, FormProvider, useForm } from "./create-form"
|
|
6
6
|
|
|
7
|
-
const NAME_LABEL = /Name/
|
|
7
|
+
const NAME_LABEL = /Name/u
|
|
8
8
|
|
|
9
9
|
let addSpy: ReturnType<typeof vi.spyOn>
|
|
10
10
|
let removeSpy: ReturnType<typeof vi.spyOn>
|
|
@@ -30,8 +30,8 @@ function Harness({ mode }: { mode?: ValidationMode }) {
|
|
|
30
30
|
)
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
const ERROR = /Enter a valid email/
|
|
34
|
-
const EMAIL_LABEL = /Email/
|
|
33
|
+
const ERROR = /Enter a valid email/iu
|
|
34
|
+
const EMAIL_LABEL = /Email/u
|
|
35
35
|
|
|
36
36
|
function getInput() {
|
|
37
37
|
return screen.getByLabelText(EMAIL_LABEL) as HTMLInputElement
|
package/src/with-form.test.tsx
CHANGED