/** * Dialog morfo — canonical first example. * * Authored from the current `dialog-provider.svelte.ts` + `sema_pre.md` * guidelines (data-last-action for asymmetric exits, transition markers). * * When the provider is refactored to consume `createAttrs(dialogMorfo)` + * `registerContract(dialogMorfo)`, this becomes the single source of * truth for parts, data-attrs, ARIA contract, keyboard, and focus. */ import type { Morfo } from '../types'; import { v } from '../types'; export const dialogMorfo = { name: 'Dialog', kebab: 'dialog', scope: ['soma', 'sema'], apg: 'https://www.w3.org/WAI/ARIA/apg/patterns/dialog/', events: [ { name: 'open', target: v.partRef('content'), semantic: { family: 'emerge' }, commits: { part: v.partRef('content'), attr: 'data-state', value: 'open' } }, { name: 'close-save', target: v.partRef('content'), semantic: { family: 'commit', intent: 'fulfill' }, regime: 'lock', prewrite: [{ part: v.partRef('content'), attr: 'data-last-action', value: 'saved' }], commits: { part: v.partRef('content'), attr: 'data-state', value: 'closed' } }, { name: 'close-cancel', target: v.partRef('content'), semantic: { family: 'emerge' }, regime: 'lock', prewrite: [{ part: v.partRef('content'), attr: 'data-last-action', value: 'cancelled' }], commits: { part: v.partRef('content'), attr: 'data-state', value: 'closed' } }, { name: 'close-dismiss', target: v.partRef('content'), semantic: { family: 'emerge' }, regime: 'lock', prewrite: [{ part: v.partRef('content'), attr: 'data-last-action', value: 'dismissed' }], commits: { part: v.partRef('content'), attr: 'data-state', value: 'closed' } }, { name: 'close-dismiss-outside', target: v.partRef('content'), semantic: { family: 'emerge' }, regime: 'lock', prewrite: [ { part: v.partRef('content'), attr: 'data-last-action', value: 'dismissed-outside' } ], commits: { part: v.partRef('content'), attr: 'data-state', value: 'closed' } }, { name: 'close-after-fail', target: v.partRef('content'), semantic: { family: 'alert', intent: 'threat' }, regime: 'lock', prewrite: [{ part: v.partRef('content'), attr: 'data-last-action', value: 'failed' }], commits: { part: v.partRef('content'), attr: 'data-state', value: 'closed' } } ], focus: { initial: 'first-focusable', trap: true, return: 'trigger', restore: true }, parts: [ { name: 'Provider', kebab: 'provider', kind: 'virtual', defaultElement: 'none', optional: false, states: ['open', 'closed'], data: [ { attr: 'data-state', values: ['open', 'closed'], value: v.stateRef('open') }, { attr: 'data-disabled', value: v.propRef('disabled'), severity: 'optional' } ], aria: [] }, { name: 'Trigger', kebab: 'trigger', kind: 'public', defaultElement: 'button', role: 'button', optional: false, states: ['open', 'closed'], data: [{ attr: 'data-state', values: ['open', 'closed'], value: v.stateRef('open') }], aria: [ { attr: 'type', value: v.literal('button') }, { attr: 'aria-haspopup', value: v.literal('dialog') }, { attr: 'aria-expanded', value: v.stateRef('open') }, { attr: 'aria-controls', value: v.partRef('content') } ] }, { name: 'Content', kebab: 'content', kind: 'public', defaultElement: 'div', role: 'dialog', optional: false, supportsNesting: true, states: ['open', 'closed'], data: [ { attr: 'data-state', values: ['open', 'closed'], value: v.stateRef('open') }, { /** * Sema alignment (sema_pre.md §9): causal exit reason. Updated * BEFORE `data-state` flips to `'closed'` so Sema can tint the * exit animation per-action. Ordering is validated by a * dedicated MutationObserver test in the smoke suite. */ attr: 'data-last-action', values: ['saved', 'cancelled', 'dismissed', 'dismissed-outside', 'failed'], severity: 'optional' }, { attr: 'data-nested', severity: 'optional' }, { attr: 'data-nested-open', severity: 'optional' }, { attr: 'data-starting-style', severity: 'optional', condition: { when: 'state-equals', state: 'open', value: 'starting' } }, { attr: 'data-ending-style', severity: 'optional', condition: { when: 'state-equals', state: 'closed', value: 'ending' } } ], aria: [ { attr: 'aria-modal', value: v.literal('true'), condition: { when: 'prop-truthy', prop: 'modal' } }, { attr: 'aria-labelledby', value: v.partRef('title'), condition: { when: 'part-present', part: 'title' }, severity: 'recommended' }, { /** * APG advises against `aria-describedby` on Dialog with rich * content (lists, tables) — AT announces everything as a * single string. Optional by design. */ attr: 'aria-describedby', value: v.partRef('description'), condition: { when: 'part-present', part: 'description' }, severity: 'optional' }, { attr: 'aria-roledescription', value: v.translationRef('#?components.dialog.content.roledescription|dialog window'), severity: 'optional' } ], keyboard: [ { key: 'Escape', action: 'close' }, { key: 'Tab', action: 'focus-next' }, { key: 'Shift+Tab', action: 'focus-prev' } ] }, { name: 'Overlay', kebab: 'overlay', kind: 'public', defaultElement: 'div', optional: true, supportsNesting: true, states: ['open', 'closed'], data: [ { attr: 'data-state', values: ['open', 'closed'], value: v.stateRef('open') }, { attr: 'data-nested', severity: 'optional' }, { attr: 'data-nested-open', severity: 'optional' }, { attr: 'data-starting-style', severity: 'optional', condition: { when: 'state-equals', state: 'open', value: 'starting' } }, { attr: 'data-ending-style', severity: 'optional', condition: { when: 'state-equals', state: 'closed', value: 'ending' } } ], aria: [{ attr: 'aria-hidden', value: v.literal('true') }] }, { /** * Title contributes to the accessible name of Content via * `aria-labelledby`. Emitted as heading for AT hierarchy, but * level is consumer-controlled (`propRef('level')`) — not a * hardcoded '2' — so a dialog can be h1 at top level or h3 in * a nested context without overriding prose. */ name: 'Title', kebab: 'title', kind: 'public', defaultElement: 'div', role: 'heading', optional: true, data: [], aria: [ { attr: 'aria-level', value: v.propRef('level'), severity: 'recommended' } ] }, { name: 'Description', kebab: 'description', kind: 'public', defaultElement: 'div', optional: true, data: [], aria: [] }, { name: 'Close', kebab: 'close', kind: 'public', defaultElement: 'button', role: 'button', optional: true, data: [], aria: [ { attr: 'type', value: v.literal('button') }, { attr: 'aria-label', value: v.propRef('aria-label'), severity: 'recommended', condition: { when: 'prop-truthy', prop: 'aria-label' } } ] } ] } as const satisfies Morfo;