You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
262 lines
6.7 KiB
262 lines
6.7 KiB
import type { Morfo } from '../types';
|
|
import { v } from '../types';
|
|
|
|
export const formMorfo = {
|
|
name: 'Form',
|
|
kebab: 'form',
|
|
scope: ['soma', 'sema'],
|
|
texts: {
|
|
label: '#?components.form.label|Form',
|
|
'error-summary-title': '#?components.form.error-summary-title|{{count}} form errors need your attention',
|
|
'error-summary-single': '#?components.form.error-summary-single|1 form error needs your attention'
|
|
},
|
|
events: [
|
|
{
|
|
name: 'commit-submit',
|
|
semantic: {
|
|
family: 'commit',
|
|
verb: 'submit',
|
|
target: v.partRef('provider'),
|
|
intent: 'fulfill',
|
|
sequence: 'post'
|
|
}
|
|
},
|
|
{
|
|
// Persistence: book §6.2 — signal.risk = untilFix. A validation
|
|
// warning must stay perceptible until the user fixes the field;
|
|
// auto-clearing after 240 ms would hide the warning the moment
|
|
// the user looks away. Form provider clears it by calling
|
|
// `runtime.clearSignal(id)` when validation passes (or
|
|
// `runtime.clearTarget(formElement)` to clear all field warnings
|
|
// at once on a successful submit).
|
|
name: 'signal-warn-invalid',
|
|
semantic: {
|
|
family: 'signal',
|
|
verb: 'warn',
|
|
target: v.partRef('provider'),
|
|
intent: 'risk',
|
|
sequence: 'post',
|
|
persistence: 'untilFix'
|
|
},
|
|
// Book §9.2: signal.warn + risk ⇒ persistentTrace +
|
|
// reducedMotionFallback='text'. Focus move is also useful here
|
|
// because the provider can land focus on the first invalid
|
|
// field; the runtime focuses `target` (the form element), and
|
|
// `focusFirstInvalid()` in the provider drills further from
|
|
// there.
|
|
a11ySemantic: {
|
|
requiresPersistentTrace: true,
|
|
requiresFocusMove: true,
|
|
reducedMotionFallback: 'text'
|
|
}
|
|
},
|
|
{
|
|
name: 'commit-reset',
|
|
semantic: {
|
|
family: 'commit',
|
|
verb: 'reset',
|
|
target: v.partRef('provider'),
|
|
intent: 'neutral',
|
|
sequence: 'post'
|
|
}
|
|
}
|
|
],
|
|
parts: [
|
|
{
|
|
name: 'Provider',
|
|
kebab: 'provider',
|
|
archetype: 'provider',
|
|
kind: 'public',
|
|
defaultElement: 'form',
|
|
optional: false,
|
|
data: [
|
|
{ attr: 'data-pending', value: v.propRef('isPending'), severity: 'optional' },
|
|
{ attr: 'data-dirty', value: v.propRef('dirty'), severity: 'optional' },
|
|
{ attr: 'data-touched', value: v.propRef('touched'), severity: 'optional' },
|
|
{ attr: 'data-invalid', value: v.propRef('invalid'), severity: 'optional' },
|
|
{ attr: 'data-submitted', value: v.propRef('submitted'), severity: 'optional' }
|
|
],
|
|
aria: [
|
|
{
|
|
attr: 'aria-busy',
|
|
value: v.literal('true'),
|
|
severity: 'optional',
|
|
condition: { when: 'prop-truthy', prop: 'isPending' }
|
|
}
|
|
]
|
|
},
|
|
{
|
|
name: 'Submit',
|
|
kebab: 'submit',
|
|
archetype: 'action',
|
|
kind: 'public',
|
|
defaultElement: 'button',
|
|
role: 'button',
|
|
optional: true,
|
|
data: [{ attr: 'data-pending', severity: 'optional' }],
|
|
aria: [
|
|
{ attr: 'type', value: v.literal('submit') },
|
|
{
|
|
attr: 'aria-label',
|
|
value: v.commonRef('buttons.submit', 'Submit'),
|
|
severity: 'recommended'
|
|
},
|
|
{
|
|
// Provider stringified this as `boolToStr(isPending)` ("true"/
|
|
// "false" ALWAYS), so it models a boolean state, not presence.
|
|
// `propRef` + `ariaBoolean` lets renderProps() emit the same
|
|
// "true"/"false" the provider used to.
|
|
attr: 'aria-busy',
|
|
value: v.propRef('isPending'),
|
|
severity: 'optional',
|
|
ariaBoolean: true
|
|
}
|
|
]
|
|
},
|
|
{
|
|
name: 'Reset',
|
|
kebab: 'reset',
|
|
archetype: 'action',
|
|
kind: 'public',
|
|
defaultElement: 'button',
|
|
role: 'button',
|
|
optional: true,
|
|
data: [{ attr: 'data-dirty', severity: 'optional' }],
|
|
aria: [
|
|
{ attr: 'type', value: v.literal('button') },
|
|
{
|
|
attr: 'aria-label',
|
|
value: v.commonRef('buttons.reset', 'Reset'),
|
|
severity: 'recommended'
|
|
}
|
|
]
|
|
},
|
|
{
|
|
name: 'ErrorSummary',
|
|
kebab: 'error-summary',
|
|
archetype: 'description',
|
|
kind: 'public',
|
|
defaultElement: 'div',
|
|
role: 'alert',
|
|
optional: true,
|
|
data: [{ attr: 'data-hidden', severity: 'optional' }],
|
|
aria: [{ attr: 'aria-live', value: v.literal('polite') }]
|
|
},
|
|
{
|
|
name: 'AutoFieldsGroup',
|
|
kebab: 'auto-fields-group',
|
|
archetype: 'group',
|
|
kind: 'public',
|
|
defaultElement: 'div',
|
|
optional: true,
|
|
data: [{ attr: 'data-form-auto-fields-group', emit: 'value' }],
|
|
aria: []
|
|
},
|
|
{
|
|
name: 'AutoFieldsArray',
|
|
kebab: 'auto-fields-array',
|
|
archetype: 'group',
|
|
kind: 'public',
|
|
defaultElement: 'div',
|
|
optional: true,
|
|
data: [{ attr: 'data-form-auto-fields-array', emit: 'value' }],
|
|
aria: []
|
|
},
|
|
{
|
|
name: 'AutoFieldsArrayToolbar',
|
|
kebab: 'auto-fields-array-toolbar',
|
|
archetype: 'header',
|
|
kind: 'public',
|
|
defaultElement: 'div',
|
|
optional: true,
|
|
data: [],
|
|
aria: []
|
|
},
|
|
{
|
|
name: 'AutoFieldsArrayAdd',
|
|
kebab: 'auto-fields-array-add',
|
|
archetype: 'action',
|
|
kind: 'public',
|
|
defaultElement: 'button',
|
|
role: 'button',
|
|
optional: true,
|
|
data: [{ attr: 'data-form-auto-fields-array-add', emit: 'value' }],
|
|
aria: [{ attr: 'type', value: v.literal('button') }]
|
|
},
|
|
{
|
|
name: 'AutoFieldsItem',
|
|
kebab: 'auto-fields-item',
|
|
archetype: 'item',
|
|
kind: 'public',
|
|
defaultElement: 'div',
|
|
optional: true,
|
|
data: [{ attr: 'data-form-auto-fields-item', emit: 'value' }],
|
|
aria: []
|
|
},
|
|
{
|
|
name: 'AutoFieldsItemToolbar',
|
|
kebab: 'auto-fields-item-toolbar',
|
|
archetype: 'header',
|
|
kind: 'public',
|
|
defaultElement: 'div',
|
|
optional: true,
|
|
data: [],
|
|
aria: []
|
|
},
|
|
{
|
|
name: 'AutoFieldsArrayRemove',
|
|
kebab: 'auto-fields-array-remove',
|
|
archetype: 'action',
|
|
kind: 'public',
|
|
defaultElement: 'button',
|
|
role: 'button',
|
|
optional: true,
|
|
data: [{ attr: 'data-form-auto-fields-array-remove', emit: 'value' }],
|
|
aria: [{ attr: 'type', value: v.literal('button') }]
|
|
},
|
|
{
|
|
name: 'AutoFieldsDiscriminated',
|
|
kebab: 'auto-fields-discriminated',
|
|
archetype: 'group',
|
|
kind: 'public',
|
|
defaultElement: 'div',
|
|
optional: true,
|
|
data: [{ attr: 'data-form-auto-fields-discriminated', emit: 'value' }],
|
|
aria: []
|
|
},
|
|
{
|
|
name: 'AutoFieldsPlaceholder',
|
|
kebab: 'auto-fields-placeholder',
|
|
archetype: 'fallback',
|
|
kind: 'public',
|
|
defaultElement: 'div',
|
|
optional: true,
|
|
data: [{ attr: 'data-form-auto-fields-placeholder', emit: 'value' }],
|
|
aria: []
|
|
},
|
|
{
|
|
name: 'AutoFieldsField',
|
|
kebab: 'auto-fields-field',
|
|
archetype: 'group',
|
|
kind: 'public',
|
|
defaultElement: 'div',
|
|
optional: true,
|
|
data: [{ attr: 'data-form-auto-fields-field', emit: 'value' }],
|
|
aria: []
|
|
},
|
|
{
|
|
name: 'AutoFieldsWidget',
|
|
kebab: 'auto-fields-widget',
|
|
archetype: 'input',
|
|
kind: 'public',
|
|
defaultElement: 'input',
|
|
optional: true,
|
|
data: [
|
|
{ attr: 'data-form-auto-fields-widget', emit: 'value' },
|
|
{ attr: 'data-form-auto-fields-widget-key', emit: 'value' }
|
|
],
|
|
aria: []
|
|
}
|
|
]
|
|
} as const satisfies Morfo;
|