From 019413501bdb5a47a7ae2842db72f078999d5750 Mon Sep 17 00:00:00 2001 From: dev Date: Thu, 11 Jun 2026 23:24:27 +0200 Subject: [PATCH] =?UTF-8?q?feat(morfo):=20ariaBoolean=20entry=20flag=20?= =?UTF-8?q?=E2=80=94=20full=20de-dup=20of=20NumberField=20Input=20aria?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `MorfoAriaEntry.ariaBoolean?: boolean` makes a propRef-valued ARIA attr compile to aria-boolean mode (renders the "true"/"false" string), the same shape a stateRef already gets. Without it, `aria-required` ← `required` prop compiled to raw mode → renderProps() emitted a boolean (DOM-identical, but a different props shape, and it forced the provider to keep stringifying). - types/schema/compile wired for the flag. - NumberField morfo: aria-required/disabled/readonly/invalid gain ariaBoolean. - NumberField Input provider: dropped the 4 boolToStr overrides — role + every aria + data-spin-field-input now come from renderProps(); soma keeps only the formatted aria-valuetext, aria-describedby, native form attrs and handlers. The Input is now fully morfo-sourced (zero re-declared attrs). Verified in browser (aria-required="false" string, role/valuenow from morfo) + provider tests 69/69. This is the reusable mechanism for de-duping aria booleans across the rollout. Co-Authored-By: Claude Opus 4.8 --- src/uix/morfo/compile.ts | 3 +- src/uix/morfo/components/number-field.ts | 28 ++++++++++++++++--- src/uix/morfo/schema.ts | 3 +- src/uix/morfo/types.ts | 7 +++++ .../number-field-provider.svelte.ts | 9 ------ 5 files changed, 35 insertions(+), 15 deletions(-) diff --git a/src/uix/morfo/compile.ts b/src/uix/morfo/compile.ts index 5fe3cc83a..04d341571 100644 --- a/src/uix/morfo/compile.ts +++ b/src/uix/morfo/compile.ts @@ -522,7 +522,8 @@ function compileDataPlan(data: MorfoData, componentKebab: string): AttrPlan { function compileAriaPlan(aria: MorfoAriaEntry, componentKebab: string): AttrPlan { const condition: ConditionPlan = aria.condition ?? 'always' const source = normalizeSource(aria.value, componentKebab) - const mode: AttrMode = aria.value.kind === 'stateRef' ? 'aria-boolean' : 'string' + const mode: AttrMode = + aria.value.kind === 'stateRef' || aria.ariaBoolean ? 'aria-boolean' : 'string' return Object.freeze({ attr: aria.attr, source, diff --git a/src/uix/morfo/components/number-field.ts b/src/uix/morfo/components/number-field.ts index cd9dcb826..5691063b6 100644 --- a/src/uix/morfo/components/number-field.ts +++ b/src/uix/morfo/components/number-field.ts @@ -92,10 +92,30 @@ export const numberFieldMorfo = { condition: { when: 'prop-defined', prop: 'max' } }, { attr: 'aria-valuetext', value: v.propRef('value'), severity: 'recommended' }, - { attr: 'aria-required', value: v.propRef('required'), severity: 'optional' }, - { attr: 'aria-disabled', value: v.propRef('disabled'), severity: 'optional' }, - { attr: 'aria-readonly', value: v.propRef('readonly'), severity: 'optional' }, - { attr: 'aria-invalid', value: v.propRef('invalid'), severity: 'optional' } + { + attr: 'aria-required', + value: v.propRef('required'), + severity: 'optional', + ariaBoolean: true + }, + { + attr: 'aria-disabled', + value: v.propRef('disabled'), + severity: 'optional', + ariaBoolean: true + }, + { + attr: 'aria-readonly', + value: v.propRef('readonly'), + severity: 'optional', + ariaBoolean: true + }, + { + attr: 'aria-invalid', + value: v.propRef('invalid'), + severity: 'optional', + ariaBoolean: true + } ], keyboard: [ { key: 'ArrowUp', action: 'increment' }, diff --git a/src/uix/morfo/schema.ts b/src/uix/morfo/schema.ts index 946e2da0e..d8f9fb573 100644 --- a/src/uix/morfo/schema.ts +++ b/src/uix/morfo/schema.ts @@ -165,7 +165,8 @@ const ariaEntrySchema = object({ attr: string(), value: valueSourceSchema, condition: optional(conditionSchema), - severity: optional(severitySchema) + severity: optional(severitySchema), + ariaBoolean: optional(boolean()) }); const keyboardSchema = object({ diff --git a/src/uix/morfo/types.ts b/src/uix/morfo/types.ts index 838a2d230..4c9976ab2 100644 --- a/src/uix/morfo/types.ts +++ b/src/uix/morfo/types.ts @@ -264,6 +264,13 @@ export interface MorfoAriaEntry { condition?: MorfoCondition; /** Validator severity. Defaults to `'required'`. */ severity?: MorfoSeverity; + /** + * Render the value as an ARIA boolean (`"true"`/`"false"` string) instead of + * passing it through raw. A `stateRef` value already compiles to aria-boolean; + * set this for a `propRef` boolean (e.g. `aria-required` ← `required` prop) so + * `renderProps()` stringifies it and the provider need not re-declare it. + */ + ariaBoolean?: boolean; } // ── Keyboard ────────────────────────────────────────────────────────────── diff --git a/src/uix/soma/components/number-field/number-field-provider.svelte.ts b/src/uix/soma/components/number-field/number-field-provider.svelte.ts index 4764c1cfe..cde5fa552 100644 --- a/src/uix/soma/components/number-field/number-field-provider.svelte.ts +++ b/src/uix/soma/components/number-field/number-field-provider.svelte.ts @@ -617,15 +617,6 @@ export class NumberFieldInputProvider { this.provider.opts.value.current !== undefined ? this.provider.formatValue(this.provider.opts.value.current) : undefined, - // ARIA booleans: the morfo declares them (propRef), but a propRef-valued - // aria attr compiles to raw mode → renderProps emits the boolean. ARIA - // wants the "true"/"false" string, so soma stringifies (same role as the - // formatted aria-valuetext above). A future `v.ariaBool()` morfo helper - // would let these resolve from the morfo too. - 'aria-required': boolToStr(this.provider.isRequired), - 'aria-disabled': boolToStr(this.provider.isDisabled), - 'aria-readonly': boolToStr(this.provider.isReadonly), - 'aria-invalid': boolToStr(this.provider.isInvalid), 'aria-describedby': this.describedBy, disabled: this.provider.isDisabled || undefined, readonly: this.provider.isReadonly || undefined,