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.
166 lines
4.8 KiB
166 lines
4.8 KiB
import type { Morfo } from '../types';
|
|
import { v } from '../types';
|
|
|
|
export const radioGroupMorfo = {
|
|
name: 'RadioGroup',
|
|
kebab: 'radio-group',
|
|
scope: ['soma', 'sema', 'eidos'],
|
|
expression: 'pack',
|
|
texts: {
|
|
label: '#?components.radio-group.label|Radio Group'
|
|
},
|
|
apg: 'https://www.w3.org/WAI/ARIA/apg/patterns/radio/',
|
|
events: [
|
|
{
|
|
// Selecting a radio is always an affirmation — there is no
|
|
// "unselect" path (the only way to unset is to select another
|
|
// option). Intent affirm carries the slight positive lift via
|
|
// SEMA_MAP.intents.affirm.deltas.
|
|
name: 'commit-select',
|
|
semantic: {
|
|
family: 'commit',
|
|
verb: 'select',
|
|
target: v.partRef('item'),
|
|
intent: 'affirm',
|
|
sequence: 'pre'
|
|
}
|
|
}
|
|
],
|
|
parts: [
|
|
{
|
|
name: 'Provider',
|
|
kebab: 'provider',
|
|
archetype: 'provider',
|
|
kind: 'public',
|
|
defaultElement: 'div',
|
|
role: 'radiogroup',
|
|
optional: false,
|
|
data: [
|
|
{
|
|
attr: 'data-orientation',
|
|
values: ['horizontal', 'vertical'],
|
|
value: v.propRef('orientation')
|
|
},
|
|
{ attr: 'data-disabled', value: v.propRef('disabled'), severity: 'optional' },
|
|
{ attr: 'data-required', value: v.propRef('required'), severity: 'optional' },
|
|
{ attr: 'data-readonly', value: v.propRef('readonly'), severity: 'optional' },
|
|
{ attr: 'data-invalid', value: v.propRef('invalid'), severity: 'optional' }
|
|
],
|
|
aria: [
|
|
{ attr: 'aria-orientation', value: v.propRef('orientation') },
|
|
{
|
|
attr: 'aria-label',
|
|
value: v.propRef('ariaLabel'),
|
|
condition: { when: 'prop-truthy', prop: 'ariaLabel' },
|
|
severity: 'recommended'
|
|
},
|
|
{
|
|
attr: 'aria-labelledby',
|
|
value: v.propRef('ariaLabelledby'),
|
|
condition: { when: 'prop-truthy', prop: 'ariaLabelledby' },
|
|
severity: 'recommended'
|
|
},
|
|
{
|
|
attr: 'aria-required',
|
|
value: v.literal('true'),
|
|
severity: 'optional',
|
|
condition: { when: 'prop-truthy', prop: 'required' }
|
|
},
|
|
{
|
|
attr: 'aria-disabled',
|
|
value: v.literal('true'),
|
|
severity: 'optional',
|
|
condition: { when: 'prop-truthy', prop: 'disabled' }
|
|
},
|
|
{
|
|
attr: 'aria-readonly',
|
|
value: v.literal('true'),
|
|
severity: 'optional',
|
|
condition: { when: 'prop-truthy', prop: 'readonly' }
|
|
},
|
|
{
|
|
attr: 'aria-invalid',
|
|
value: v.literal('true'),
|
|
severity: 'optional',
|
|
condition: { when: 'prop-truthy', prop: 'invalid' }
|
|
}
|
|
]
|
|
},
|
|
{
|
|
name: 'Item',
|
|
kebab: 'item',
|
|
archetype: 'item',
|
|
kind: 'public',
|
|
defaultElement: 'button',
|
|
role: 'radio',
|
|
optional: false,
|
|
states: ['checked', 'unchecked'],
|
|
data: [
|
|
{ attr: 'data-state', values: ['checked', 'unchecked'], value: v.stateRef('checked') },
|
|
{ attr: 'data-value' },
|
|
{ attr: 'data-disabled', value: v.propRef('disabled'), severity: 'optional' },
|
|
{ attr: 'data-readonly', value: v.propRef('readonly'), severity: 'optional' },
|
|
{
|
|
attr: 'data-orientation',
|
|
values: ['horizontal', 'vertical'],
|
|
value: v.propRef('orientation')
|
|
}
|
|
],
|
|
aria: [
|
|
{ attr: 'type', value: v.literal('button') },
|
|
{ attr: 'aria-checked', value: v.stateRef('checked') }
|
|
],
|
|
keyboard: [
|
|
{ key: 'ArrowDown', action: 'next-item' },
|
|
{ key: 'ArrowUp', action: 'prev-item' },
|
|
{ key: 'ArrowRight', action: 'next-item' },
|
|
{ key: 'ArrowLeft', action: 'prev-item' }
|
|
]
|
|
},
|
|
{
|
|
// Visual indicator (filled dot) inside an Item button. Optional
|
|
// — the eidos wrapper renders it by default with `<SvgDot>`,
|
|
// headless consumers can omit. Marker is contract-backed so
|
|
// eidos selectors don't drift.
|
|
name: 'Indicator',
|
|
kebab: 'indicator',
|
|
archetype: 'indicator',
|
|
kind: 'public',
|
|
defaultElement: 'span',
|
|
optional: true,
|
|
states: ['checked', 'unchecked'],
|
|
data: [{ attr: 'data-state', values: ['checked', 'unchecked'], value: v.stateRef('checked') }],
|
|
aria: [{ attr: 'aria-hidden', value: v.literal('true') }]
|
|
},
|
|
{
|
|
// Native `<input type="radio">` rendered alongside the button so
|
|
// the browser includes name+value in form submissions and
|
|
// participates in HTMLFormElement validation. Visually hidden,
|
|
// kept out of the AT tree (`aria-hidden`) since the button's
|
|
// role="radio" is the AT-visible control.
|
|
name: 'HiddenInput',
|
|
kebab: 'hidden-input',
|
|
archetype: 'input',
|
|
kind: 'public',
|
|
defaultElement: 'input',
|
|
optional: true,
|
|
data: [],
|
|
aria: [{ attr: 'aria-hidden', value: v.literal('true') }]
|
|
},
|
|
{
|
|
// Group-level label part. When present, the Provider gets
|
|
// `aria-labelledby="<label id>"` instead of relying on a string
|
|
// `aria-label`. Either form (string aria-label or this part) is
|
|
// required by the radiogroup APG pattern.
|
|
name: 'Label',
|
|
kebab: 'label',
|
|
archetype: 'label',
|
|
kind: 'public',
|
|
defaultElement: 'div',
|
|
optional: true,
|
|
data: [],
|
|
aria: []
|
|
}
|
|
]
|
|
} as const satisfies Morfo;
|