fix(forms): preserve regex quantifiers and use inline constraint errors
This commit is contained in:
1 parent
1dd60ded0d
commit
fe40cc56df
13 files changed
+132
-26
No files matched your search
@@ -320,7 +320,7 @@
|
|||||||
},
|
},
|
||||||
"packages/compiler": {
|
"packages/compiler": {
|
||||||
"name": "@wrnexus/compiler",
|
"name": "@wrnexus/compiler",
|
||||||
"version": "0.8.26",
|
"version": "0.8.27",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/csr": "workspace:*",
|
"@wrnexus/csr": "workspace:*",
|
||||||
"@wrnexus/store": "workspace:*",
|
"@wrnexus/store": "workspace:*",
|
||||||
@@ -688,7 +688,7 @@
|
|||||||
},
|
},
|
||||||
"packages/ui": {
|
"packages/ui": {
|
||||||
"name": "@wrnexus/ui",
|
"name": "@wrnexus/ui",
|
||||||
"version": "0.8.57",
|
"version": "0.8.58",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wrnexus/core": "workspace:*",
|
"@wrnexus/core": "workspace:*",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/compiler",
|
"name": "@wrnexus/compiler",
|
||||||
"version": "0.8.26",
|
"version": "0.8.27",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"exports": {
|
"exports": {
|
||||||
|
|||||||
@@ -912,7 +912,7 @@ function renderNestedComponentInvocation(
|
|||||||
|
|
||||||
const compiledValue = wholeExpression
|
const compiledValue = wholeExpression
|
||||||
? `\${__wrnProp(${ctx.resolveExpr(wholeExpression)})}`
|
? `\${__wrnProp(${ctx.resolveExpr(wholeExpression)})}`
|
||||||
: compileAttrValue(attr.value, ctx);
|
: compileAttrValue(attr.value, ctx, attr.name === "pattern");
|
||||||
|
|
||||||
const rendered = ` ${attr.name}="${compiledValue}"`;
|
const rendered = ` ${attr.name}="${compiledValue}"`;
|
||||||
|
|
||||||
@@ -2254,7 +2254,7 @@ function compileText(raw: string, ctx: CompCtx): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Compile an attribute value; `{expr}` is baked server-side (loop vars stay literal). */
|
/** Compile an attribute value; `{expr}` is baked server-side (loop vars stay literal). */
|
||||||
function compileAttrValue(raw: string, ctx: CompCtx): string {
|
function compileAttrValue(raw: string, ctx: CompCtx, regexPattern = false): string {
|
||||||
if (!raw.includes("{")) return escLit(attrEscape(raw));
|
if (!raw.includes("{")) return escLit(attrEscape(raw));
|
||||||
let out = "";
|
let out = "";
|
||||||
let last = 0;
|
let last = 0;
|
||||||
@@ -2263,7 +2263,9 @@ function compileAttrValue(raw: string, ctx: CompCtx): string {
|
|||||||
while ((m = INTERP_RE.exec(raw))) {
|
while ((m = INTERP_RE.exec(raw))) {
|
||||||
out += escLit(attrEscape(raw.slice(last, m.index)));
|
out += escLit(attrEscape(raw.slice(last, m.index)));
|
||||||
const expr = m[1]!.trim();
|
const expr = m[1]!.trim();
|
||||||
if (ctx.loopVars && exprRefsState(expr, ctx.loopVars)) {
|
if (regexPattern && /^\d+(?:,\d*)?$/.test(expr)) {
|
||||||
|
out += escLit(attrEscape(m[0])); // Regex quantifiers are not template expressions.
|
||||||
|
} else if (ctx.loopVars && exprRefsState(expr, ctx.loopVars)) {
|
||||||
out += escLit(`{${expr}}`); // hydrated per-item by the list renderer
|
out += escLit(`{${expr}}`); // hydrated per-item by the list renderer
|
||||||
} else {
|
} else {
|
||||||
out += `\${__wrnAttr(${ctx.resolveExpr(expr)})}`;
|
out += `\${__wrnAttr(${ctx.resolveExpr(expr)})}`;
|
||||||
@@ -2628,7 +2630,7 @@ function renderComponentNode(node: ViewNode, ctx: CompCtx): string {
|
|||||||
const compiledValue =
|
const compiledValue =
|
||||||
isExplicitComponentMount && wholeExpression
|
isExplicitComponentMount && wholeExpression
|
||||||
? `\${__wrnProp(${elementContext.resolveExpr(wholeExpression)})}`
|
? `\${__wrnProp(${elementContext.resolveExpr(wholeExpression)})}`
|
||||||
: compileAttrValue(a.value, elementContext);
|
: compileAttrValue(a.value, elementContext, a.name === "pattern");
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* A constraint attribute whose value is only known at render time (a
|
* A constraint attribute whose value is only known at render time (a
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { test, expect } from "bun:test";
|
||||||
|
import { parse, generate } from "../src/index.ts";
|
||||||
|
|
||||||
|
test("HTML and component pattern attributes preserve regex quantifiers", () => {
|
||||||
|
for (const tag of ["input", "Input"]) {
|
||||||
|
for (const pattern of ["[0-9]{6}", "[a-z]{2,8}", "[A-Z]{1,}"]) {
|
||||||
|
const code = generate(parse(`page Example { view { <${tag} pattern="${pattern}" /> } }`));
|
||||||
|
expect(code).toContain(pattern);
|
||||||
|
expect(code).not.toContain("__wrnAttr(6)");
|
||||||
|
expect(code).not.toContain("__wrnAttr(2,8)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("dynamic pattern props still compile as expressions", () => {
|
||||||
|
const code = generate(
|
||||||
|
parse(
|
||||||
|
'component Example { props { pattern: string = "" } view { <input pattern="{pattern}" /> } }',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
expect(code).toContain("__wrnAttr(pattern)");
|
||||||
|
});
|
||||||
@@ -413,7 +413,7 @@ Theme-aware, responsive file input component.
|
|||||||
Theme-aware, responsive input component.
|
Theme-aware, responsive input component.
|
||||||
|
|
||||||
- Mount: `data-component="Input"`
|
- Mount: `data-component="Input"`
|
||||||
- Props: `size: string = "default"`, `color: string = "primary"`, `id: string = ""`, `name: string = ""`, `label: string = "Input"`, `hiddenLabel: boolean = false`, `placeholder: string = ""`, `value: string = ""`, `type: string = "text"`, `variant: string = "normal"`, `icon: string = ""`, `iconPosition: string = "start"`, `helperText: string = ""`, `cornerHint: string = ""`, `error: string = ""`, `inline: boolean = false`, `readonly: boolean = false`, `disabled: boolean = false`, `required: boolean = false`, `autocomplete: string = ""`, `inputmode: string = ""`, `minlength: string = ""`, `maxlength: string = ""`, `pattern: string = ""`, `min: string = ""`, `max: string = ""`, `step: string = ""`, `class: string = ""`
|
- Props: `size: string = "default"`, `color: string = "primary"`, `id: string = ""`, `name: string = ""`, `label: string = "Input"`, `hiddenLabel: boolean = false`, `placeholder: string = ""`, `value: string = ""`, `type: string = "text"`, `variant: string = "normal"`, `icon: string = ""`, `iconPosition: string = "start"`, `helperText: string = ""`, `cornerHint: string = ""`, `error: string = ""`, `invalidMessage: string = ""`, `inline: boolean = false`, `readonly: boolean = false`, `disabled: boolean = false`, `required: boolean = false`, `autocomplete: string = ""`, `inputmode: string = ""`, `minlength: string = ""`, `maxlength: string = ""`, `pattern: string = ""`, `min: string = ""`, `max: string = ""`, `step: string = ""`, `class: string = ""`
|
||||||
- Slots: None
|
- Slots: None
|
||||||
- Outputs: `input({ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])`, `change({ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])`, `focus({ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])`, `blur({ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])`, `invalid({ value: string | number | boolean | null | object; name: string; message: string; sourceEvent: Event })`, `keydown({ key: string; value: string | number | boolean | null | object; name: string; sourceEvent: Event })`, `keyup({ key: string; value: string | number | boolean | null | object; name: string; sourceEvent: Event })`
|
- Outputs: `input({ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])`, `change({ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])`, `focus({ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])`, `blur({ sourceEvent?: Event; value: string | number | boolean | null | object; [key: string]: string | number | boolean | null | object } | string | number | boolean | null | object[])`, `invalid({ value: string | number | boolean | null | object; name: string; message: string; sourceEvent: Event })`, `keydown({ key: string; value: string | number | boolean | null | object; name: string; sourceEvent: Event })`, `keyup({ key: string; value: string | number | boolean | null | object; name: string; sourceEvent: Event })`
|
||||||
|
|
||||||
|
|||||||
@@ -7065,6 +7065,13 @@
|
|||||||
"default": "\"\"",
|
"default": "\"\"",
|
||||||
"options": []
|
"options": []
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "invalidMessage",
|
||||||
|
"type": "string",
|
||||||
|
"required": false,
|
||||||
|
"default": "\"\"",
|
||||||
|
"options": []
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "inline",
|
"name": "inline",
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
|
|||||||
@@ -41,7 +41,16 @@ component StrongPassword {
|
|||||||
state detailsOpen: boolean = false
|
state detailsOpen: boolean = false
|
||||||
state revealed: boolean = false
|
state revealed: boolean = false
|
||||||
|
|
||||||
|
state nativeError: string = ""
|
||||||
|
|
||||||
functions {
|
functions {
|
||||||
|
client function handleInvalid(sourceEvent) {
|
||||||
|
sourceEvent.preventDefault()
|
||||||
|
sourceEvent.stopPropagation()
|
||||||
|
nativeError = validationMessage || (sourceEvent.currentTarget.validity.valueMissing ? "Enter your password." : "Enter a password that meets the requirements.")
|
||||||
|
sourceEvent.currentTarget.setAttribute("aria-invalid", "true")
|
||||||
|
}
|
||||||
|
|
||||||
shared function hasMinimumLength() {
|
shared function hasMinimumLength() {
|
||||||
return password.length >= Number(minLength)
|
return password.length >= Number(minLength)
|
||||||
}
|
}
|
||||||
@@ -146,11 +155,11 @@ component StrongPassword {
|
|||||||
disabled="{disabled}"
|
disabled="{disabled}"
|
||||||
readonly="{readonly}"
|
readonly="{readonly}"
|
||||||
required="{required}"
|
required="{required}"
|
||||||
aria-invalid="{invalid ? 'true' : 'false'}"
|
aria-invalid="{invalid || nativeError ? 'true' : 'false'}"
|
||||||
aria-describedby="{validationMessage ? name + '-validation' : hintText ? name + '-hint' : ''}"
|
aria-describedby="{(validationMessage || nativeError) ? name + '-validation' : hintText ? name + '-hint' : ''}"
|
||||||
@focus="detailsOpen = true"
|
@focus="detailsOpen = true"
|
||||||
@blur="detailsOpen = false"
|
@blur="detailsOpen = false"
|
||||||
@input="password = event.target.value; output.input({ value: password, score: score(), maximumScore: maximumScore(), percent: strengthPercent(), level: strengthLevel() }); output.strength({ value: password, score: score(), maximumScore: maximumScore(), percent: strengthPercent(), level: strengthLevel() })"
|
@invalid="handleInvalid(event)" @input="nativeError = ''; password = event.target.value; output.input({ value: password, score: score(), maximumScore: maximumScore(), percent: strengthPercent(), level: strengthLevel() }); output.strength({ value: password, score: score(), maximumScore: maximumScore(), percent: strengthPercent(), level: strengthLevel() })"
|
||||||
@change="output.change({ value: password, score: score(), maximumScore: maximumScore(), percent: strengthPercent(), level: strengthLevel() })"
|
@change="output.change({ value: password, score: score(), maximumScore: maximumScore(), percent: strengthPercent(), level: strengthLevel() })"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
@@ -231,7 +240,7 @@ component StrongPassword {
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<small id="{name}-validation" class="wrn-next__validation" data-error="{name}">{validationMessage}</small>
|
<small id="{name}-validation" class="wrn-next__validation" data-error="{name}">{(validationMessage || nativeError)}</small>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,16 @@ component TogglePassword {
|
|||||||
|
|
||||||
state revealed = visible
|
state revealed = visible
|
||||||
|
|
||||||
|
state nativeError: string = ""
|
||||||
|
|
||||||
functions {
|
functions {
|
||||||
|
client function handleInvalid(sourceEvent) {
|
||||||
|
sourceEvent.preventDefault()
|
||||||
|
sourceEvent.stopPropagation()
|
||||||
|
nativeError = validationMessage || (sourceEvent.currentTarget.validity.valueMissing ? "Enter your password." : "Enter a password that meets the requirements.")
|
||||||
|
sourceEvent.currentTarget.setAttribute("aria-invalid", "true")
|
||||||
|
}
|
||||||
|
|
||||||
shared function toggleVisibility() {
|
shared function toggleVisibility() {
|
||||||
if (disabled || readonly || !toggleable) {
|
if (disabled || readonly || !toggleable) {
|
||||||
return
|
return
|
||||||
@@ -78,8 +87,8 @@ component TogglePassword {
|
|||||||
disabled="{disabled || field.disabled}"
|
disabled="{disabled || field.disabled}"
|
||||||
readonly="{readonly || field.readonly}"
|
readonly="{readonly || field.readonly}"
|
||||||
required="{required || field.required}"
|
required="{required || field.required}"
|
||||||
aria-invalid="{invalid ? 'true' : 'false'}"
|
aria-invalid="{invalid || nativeError ? 'true' : 'false'}"
|
||||||
@input="output.input({ name: event.target.name, value: event.target.value, visible: revealed })"
|
@invalid="handleInvalid(event)" @input="nativeError = ''; output.input({ name: event.target.name, value: event.target.value, visible: revealed })"
|
||||||
@change="output.change({ name: event.target.name, value: event.target.value, visible: revealed })"
|
@change="output.change({ name: event.target.name, value: event.target.value, visible: revealed })"
|
||||||
/>
|
/>
|
||||||
{#if toggleable && toggleMode === "button"}
|
{#if toggleable && toggleMode === "button"}
|
||||||
@@ -122,9 +131,9 @@ component TogglePassword {
|
|||||||
disabled="{disabled}"
|
disabled="{disabled}"
|
||||||
readonly="{readonly}"
|
readonly="{readonly}"
|
||||||
required="{required}"
|
required="{required}"
|
||||||
aria-invalid="{invalid ? 'true' : 'false'}"
|
aria-invalid="{invalid || nativeError ? 'true' : 'false'}"
|
||||||
aria-describedby="{validationMessage ? name + '-validation' : helpText ? name + '-help' : ''}"
|
aria-describedby="{(validationMessage || nativeError) ? name + '-validation' : helpText ? name + '-help' : ''}"
|
||||||
@input="output.input({ value: event.target.value, visible: revealed })"
|
@invalid="handleInvalid(event)" @input="nativeError = ''; output.input({ value: event.target.value, visible: revealed })"
|
||||||
@change="output.change({ value: event.target.value, visible: revealed })"
|
@change="output.change({ value: event.target.value, visible: revealed })"
|
||||||
/>
|
/>
|
||||||
{#if toggleable && toggleMode === "button"}
|
{#if toggleable && toggleMode === "button"}
|
||||||
@@ -153,12 +162,12 @@ component TogglePassword {
|
|||||||
<span>{checkboxLabel}</span>
|
<span>{checkboxLabel}</span>
|
||||||
</label>
|
</label>
|
||||||
{/if}
|
{/if}
|
||||||
{#if helpText && !validationMessage}<small id="{name}-help">{helpText}</small>{/if}
|
{#if helpText && !(validationMessage || nativeError)}<small id="{name}-help">{helpText}</small>{/if}
|
||||||
<small
|
<small
|
||||||
id="{name}-validation"
|
id="{name}-validation"
|
||||||
class="wrn-next__validation"
|
class="wrn-next__validation"
|
||||||
data-error="{name}"
|
data-error="{name}"
|
||||||
>{validationMessage}</small>
|
>{(validationMessage || nativeError)}</small>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ component Input {
|
|||||||
helperText: string = ""
|
helperText: string = ""
|
||||||
cornerHint: string = ""
|
cornerHint: string = ""
|
||||||
error: string = ""
|
error: string = ""
|
||||||
|
invalidMessage: string = ""
|
||||||
inline: boolean = false
|
inline: boolean = false
|
||||||
readonly: boolean = false
|
readonly: boolean = false
|
||||||
disabled: boolean = false
|
disabled: boolean = false
|
||||||
@@ -56,7 +57,16 @@ component Input {
|
|||||||
client function handleChange(sourceEvent) { sourceEvent.stopPropagation(); output.change(detail(sourceEvent)) }
|
client function handleChange(sourceEvent) { sourceEvent.stopPropagation(); output.change(detail(sourceEvent)) }
|
||||||
client function handleFocus(sourceEvent) { sourceEvent.stopPropagation(); output.focus(detail(sourceEvent)) }
|
client function handleFocus(sourceEvent) { sourceEvent.stopPropagation(); output.focus(detail(sourceEvent)) }
|
||||||
client function handleBlur(sourceEvent) { sourceEvent.stopPropagation(); output.blur(detail(sourceEvent)) }
|
client function handleBlur(sourceEvent) { sourceEvent.stopPropagation(); output.blur(detail(sourceEvent)) }
|
||||||
client function handleInvalid(sourceEvent) { sourceEvent.stopPropagation(); validationError = sourceEvent.currentTarget.validationMessage; sourceEvent.currentTarget.setAttribute("aria-invalid", "true"); sourceEvent.currentTarget.closest(".wrn-next--field").setAttribute("data-invalid", "true"); output.invalid({ value: sourceEvent.currentTarget.value, name: name, message: validationError, sourceEvent: sourceEvent }) }
|
client function handleInvalid(sourceEvent) {
|
||||||
|
sourceEvent.preventDefault()
|
||||||
|
sourceEvent.stopPropagation()
|
||||||
|
const field = sourceEvent.currentTarget
|
||||||
|
const validity = field.validity
|
||||||
|
validationError = error || invalidMessage || (validity.valueMissing ? "This field is required." : validity.typeMismatch ? "Enter a valid " + (type === "email" ? "email address." : "value.") : validity.tooShort ? "Enter at least " + minlength + " characters." : validity.tooLong ? "Enter no more than " + maxlength + " characters." : validity.patternMismatch ? "Enter a value in the required format." : validity.rangeUnderflow ? "Enter a value of at least " + min + "." : validity.rangeOverflow ? "Enter a value no greater than " + max + "." : "Enter a valid value.")
|
||||||
|
field.setAttribute("aria-invalid", "true")
|
||||||
|
field.closest(".wrn-next--field").setAttribute("data-invalid", "true")
|
||||||
|
output.invalid({ value: field.value, name: name, message: validationError, sourceEvent: sourceEvent })
|
||||||
|
}
|
||||||
client function handleKeydown(sourceEvent) { sourceEvent.stopPropagation(); output.keydown({ key: sourceEvent.key, value: sourceEvent.currentTarget.value, name: name, sourceEvent: sourceEvent }) }
|
client function handleKeydown(sourceEvent) { sourceEvent.stopPropagation(); output.keydown({ key: sourceEvent.key, value: sourceEvent.currentTarget.value, name: name, sourceEvent: sourceEvent }) }
|
||||||
client function handleKeyup(sourceEvent) { sourceEvent.stopPropagation(); output.keyup({ key: sourceEvent.key, value: sourceEvent.currentTarget.value, name: name, sourceEvent: sourceEvent }) }
|
client function handleKeyup(sourceEvent) { sourceEvent.stopPropagation(); output.keyup({ key: sourceEvent.key, value: sourceEvent.currentTarget.value, name: name, sourceEvent: sourceEvent }) }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ size: string = "default"
|
|||||||
}
|
}
|
||||||
client function handleFocus(sourceEvent) { emitField("focus", sourceEvent); output.open({ name: name, sourceEvent: sourceEvent }) }
|
client function handleFocus(sourceEvent) { emitField("focus", sourceEvent); output.open({ name: name, sourceEvent: sourceEvent }) }
|
||||||
client function handleBlur(sourceEvent) { emitField("blur", sourceEvent); output.close({ name: name, sourceEvent: sourceEvent }) }
|
client function handleBlur(sourceEvent) { emitField("blur", sourceEvent); output.close({ name: name, sourceEvent: sourceEvent }) }
|
||||||
client function handleInvalid(sourceEvent) { validationError = sourceEvent.currentTarget.validationMessage; sourceEvent.currentTarget.setAttribute("aria-invalid", "true"); sourceEvent.currentTarget.closest(".wrn-next--field").setAttribute("data-invalid", "true"); output.invalid({ name: name, message: validationError, sourceEvent: sourceEvent }) }
|
client function handleInvalid(sourceEvent) { sourceEvent.preventDefault(); sourceEvent.stopPropagation(); validationError = error || "Choose an option."; sourceEvent.currentTarget.setAttribute("aria-invalid", "true"); sourceEvent.currentTarget.closest(".wrn-next--field").setAttribute("data-invalid", "true"); output.invalid({ name: name, message: validationError, sourceEvent: sourceEvent }) }
|
||||||
}
|
}
|
||||||
view {
|
view {
|
||||||
<div {...attrs} class="wrn-component wrn-component--color-{color} wrn-component--size-{size} wrn-next--field wrn-next--select {class}" data-variant="{variant}" data-inline="{inline}" data-floating="{variant === 'floating'}" data-invalid="{error || validationError ? 'true' : 'false'}" data-readonly="{readonly}">
|
<div {...attrs} class="wrn-component wrn-component--color-{color} wrn-component--size-{size} wrn-next--field wrn-next--select {class}" data-variant="{variant}" data-inline="{inline}" data-floating="{variant === 'floating'}" data-invalid="{error || validationError ? 'true' : 'false'}" data-readonly="{readonly}">
|
||||||
|
|||||||
@@ -40,7 +40,9 @@ size: string = "default"
|
|||||||
output[nameEvent]({ value: sourceEvent.currentTarget.value, name: name, sourceEvent: sourceEvent })
|
output[nameEvent]({ value: sourceEvent.currentTarget.value, name: name, sourceEvent: sourceEvent })
|
||||||
}
|
}
|
||||||
client function handleInvalid(sourceEvent) {
|
client function handleInvalid(sourceEvent) {
|
||||||
validationError = sourceEvent.currentTarget.validationMessage
|
sourceEvent.preventDefault()
|
||||||
|
sourceEvent.stopPropagation()
|
||||||
|
validationError = error || (sourceEvent.currentTarget.validity.valueMissing ? "This field is required." : "Enter text within the allowed length.")
|
||||||
sourceEvent.currentTarget.setAttribute("aria-invalid", "true")
|
sourceEvent.currentTarget.setAttribute("aria-invalid", "true")
|
||||||
sourceEvent.currentTarget.closest(".wrn-next--field").setAttribute("data-invalid", "true")
|
sourceEvent.currentTarget.closest(".wrn-next--field").setAttribute("data-invalid", "true")
|
||||||
output.invalid({ value: sourceEvent.currentTarget.value, name: name, message: validationError, sourceEvent: sourceEvent })
|
output.invalid({ value: sourceEvent.currentTarget.value, name: name, message: validationError, sourceEvent: sourceEvent })
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@wrnexus/ui",
|
"name": "@wrnexus/ui",
|
||||||
"version": "0.8.57",
|
"version": "0.8.58",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
|
|||||||
@@ -1344,7 +1344,7 @@ test("basic form fields expose shared labels, variants, states, hints, values, a
|
|||||||
expect(emitted).toEqual(["input", "change", "focus", "blur", "keydown", "keyup"]);
|
expect(emitted).toEqual(["input", "change", "focus", "blur", "keydown", "keyup"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("basic fields render native constraint messages and select matches field surfaces", async () => {
|
test("basic fields suppress native popups and render package constraint messages", async () => {
|
||||||
const inputSource = readFileSync(uiComponentPath("Input"), "utf8");
|
const inputSource = readFileSync(uiComponentPath("Input"), "utf8");
|
||||||
const dom = mountHtml(
|
const dom = mountHtml(
|
||||||
await renderComponent(inputSource, {
|
await renderComponent(inputSource, {
|
||||||
@@ -1355,9 +1355,14 @@ test("basic fields render native constraint messages and select matches field su
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
const input = dom.querySelector("input") as HTMLInputElement;
|
const input = dom.querySelector("input") as HTMLInputElement;
|
||||||
input.dispatchEvent(new (dom.window as any).Event("invalid", { bubbles: false }));
|
const invalidEvent = new (dom.window as any).Event("invalid", {
|
||||||
|
bubbles: false,
|
||||||
|
cancelable: true,
|
||||||
|
});
|
||||||
|
input.dispatchEvent(invalidEvent);
|
||||||
|
expect(invalidEvent.defaultPrevented).toBe(true);
|
||||||
expect(dom.querySelector("[data-error='name']")?.textContent?.trim()).toBe(
|
expect(dom.querySelector("[data-error='name']")?.textContent?.trim()).toBe(
|
||||||
input.validationMessage,
|
"This field is required.",
|
||||||
);
|
);
|
||||||
expect(dom.querySelector(".wrn-next--input")?.getAttribute("data-invalid")).toBe("true");
|
expect(dom.querySelector(".wrn-next--input")?.getAttribute("data-invalid")).toBe("true");
|
||||||
|
|
||||||
@@ -1367,6 +1372,46 @@ test("basic fields render native constraint messages and select matches field su
|
|||||||
expect(selectSource).toContain("wrn-next__select-indicator");
|
expect(selectSource).toContain("wrn-next__select-indicator");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("OTP inputs accept six digits and show custom inline errors without native popups", async () => {
|
||||||
|
const source = readFileSync(uiComponentPath("Input"), "utf8");
|
||||||
|
const dom = mountHtml(
|
||||||
|
await renderComponent(source, {
|
||||||
|
name: "code",
|
||||||
|
pattern: "[0-9]{6}",
|
||||||
|
required: true,
|
||||||
|
invalidMessage: "Enter the six-digit verification code.",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const input = dom.querySelector("input") as HTMLInputElement;
|
||||||
|
input.value = "737651";
|
||||||
|
expect(input.checkValidity()).toBe(true);
|
||||||
|
input.value = "123";
|
||||||
|
const event = new (dom.window as any).Event("invalid", { cancelable: true });
|
||||||
|
input.dispatchEvent(event);
|
||||||
|
expect(event.defaultPrevented).toBe(true);
|
||||||
|
expect(dom.querySelector("[data-error='code']")?.textContent).toContain(
|
||||||
|
"Enter the six-digit verification code.",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("password fields cancel native invalid popups and show inline errors", async () => {
|
||||||
|
for (const component of ["TogglePassword", "StrongPassword"]) {
|
||||||
|
const dom = mountHtml(
|
||||||
|
await renderComponent(readFileSync(uiComponentPath(component), "utf8"), {
|
||||||
|
name: "password",
|
||||||
|
required: true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const input = dom.querySelector("input") as HTMLInputElement;
|
||||||
|
const event = new (dom.window as any).Event("invalid", { cancelable: true });
|
||||||
|
input.dispatchEvent(event);
|
||||||
|
expect(event.defaultPrevented).toBe(true);
|
||||||
|
expect(dom.querySelector("[data-error='password']")?.textContent).toContain(
|
||||||
|
"Enter your password.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test("all basic form components render values and their must-have interaction events", async () => {
|
test("all basic form components render values and their must-have interaction events", async () => {
|
||||||
const components = [
|
const components = [
|
||||||
"Checkbox",
|
"Checkbox",
|
||||||
|
|||||||
Reference in new issue
Block a user