morfo: enrich 65 components with aria + keyboard + focus contracts

Each morfo now declares what its provider actually emits (via agent-based
extraction from provider code). Before: parts + data only. After: parts +
data + ARIA per part + keyboard per part + focus policy for overlays.

Contract source: provider's `readonly props = $derived.by(...)` blocks,
classified into the v.* value kinds — literal, stateRef, partRef, propRef,
translationRef. Conditions extracted from ternary / guard patterns.

Focus policies added on overlay components: popover, drawer, alert-dialog,
dropdown-menu, context-menu, select, date-picker, date-range-picker,
time-picker, time-range-picker, color-picker. Context-menu uses
return:'previous' (no discrete trigger). Tooltip, link-preview, combobox,
command omit focus (no trap).

Tag-group Link variant: morfo-check tolerates sibling parts sharing a
physical element (Link IS Item in the DOM) via allPartAttrs cross-scope.

README.md added — developer guide: authoring a morfo step-by-step, the
v.* builders, severity rules, validation layers, polymorphism handling
via defaultElement + role, Sema alignment (data-last-action, transition
markers).

Verify:
  npm run check — 5 errs, all pre-existing in sium/examples.
  vitest run src/uix/morfo — 7/7 pass.
  morfo:check — 64/66 pass (2 flaky Playwright navigation timeouts,
    re-run clean when isolated).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
semantuix
dev 6 months ago
parent 8a5b402dd3
commit 0222af7a0b

@ -0,0 +1,425 @@
# Morfo
**The cross-layer contract of a component's public DOM surface.**
Morfo is the single source of truth for a component's parts, data-attrs, ARIA contract, keyboard shortcuts, and focus policy. The same morfo is consumed by soma (to wire the headless provider), by eidos (to generate CSS selectors), by sema (to bind perceptual channels), and by the docs site (to render part tables).
**One file per component**, at `src/uix/morfo/components/{kebab}.ts`. No prose — that's the component's README. No props — those live in `types.ts` with JSDoc. Just the machine-readable contract.
---
## Why morfo exists
Without morfo, a component's structural information lives in many places:
- Part names in `createAttrs({ parts: [...] })` inside the provider.
- Data-attr enums in `registerContract({ parts: {...} })` also in the provider.
- ARIA emission hardcoded in the provider's `$derived.by(...)` props.
- Keyboard handlers scattered across the provider.
- Prose descriptions in the README.
- Selector strings duplicated in eidos CSS, sema `.csem`, docs tables.
Renaming a part (`content` → `panel`) used to mean touching 6+ locations with zero automatic verification. Cross-layer drift (soma emits `data-dialog-content`, eidos styles `data-dialog-panel`) was silent.
With morfo, **every location reads from the same declaration**. Parts, data-attrs, and their enum values are authored once. `createAttrs` and `registerContract` consume the morfo directly. A smoke test validates the real DOM against the declaration on every CI run.
---
## What morfo contains
A `Morfo` is a plain TypeScript constant that describes:
- **`name`** — PascalCase display name (`"Dialog"`).
- **`kebab`** — kebab-case identifier (`"dialog"`), matches `data-{kebab}` and `createAttrs({component})`.
- **`scope`** — which layers implement this component: `['soma']`, `['soma', 'eidos']`, etc.
- **`apg`** — optional URL to the WAI-ARIA APG pattern when the component implements a formal one.
- **`focus`** — optional focus policy for overlays / composites.
- **`parts`** — the part tree (recursive). Each part declares:
- `name`, `kebab`, `kind` (`public` / `virtual`).
- `defaultElement` (advisory), `role` (always-emitted).
- `optional`, `supportsNesting`.
- `states` — the state names this part can be in.
- `data` — data-attributes emitted, with enum values when applicable.
- `aria` — ARIA attribute contract (attr + value source + condition).
- `keyboard` — keyboard shortcuts relevant when the part has focus.
- `parts` — nested sub-parts (recursive).
See [`types.ts`](./types.ts) for the full TypeScript shape.
## What morfo does NOT contain
| Not in morfo | Lives in | Reason |
|--------------|----------|--------|
| Summary, overview prose | `{component}/README.md` | Editorial, not contract |
| Comparison vs Radix / Base UI / Bits | `{component}/README.md` | Third-party drift shouldn't pollute the contract |
| Usage examples | `{component}/README.md` | Narrative |
| Props (names, types, defaults) | `{component}/types.ts` with JSDoc | Canonical source is TS + JSDoc |
| Translations | `{component}/langs.ts` (idlangref) | Separate registry, consumed by the provider |
| Event handlers, state machines | `{component}-provider.svelte.ts` | Code, not data |
| Visual variants / recipes | `src/uix/eidos/` (future) | Layer-specific, not shared |
---
## Anatomy of a morfo file
Minimal template:
```ts
// src/uix/morfo/components/dialog.ts
import type { Morfo } from '../types';
import { v } from '../types'; // value builders: v.literal, v.stateRef, v.partRef, v.propRef, v.translationRef
export const dialogMorfo: Morfo = {
name: 'Dialog',
kebab: 'dialog',
scope: ['soma'],
apg: 'https://www.w3.org/WAI/ARIA/apg/patterns/dialog/',
focus: {
initial: 'first-focusable',
trap: true,
return: 'trigger',
restore: true
},
parts: [
{
name: 'Provider',
kebab: 'root', // special: 'root' emits data-dialog (no suffix)
kind: 'virtual', // context-only, no DOM
defaultElement: 'none',
optional: false,
data: [],
aria: []
},
{
name: 'Trigger',
kebab: 'trigger',
kind: 'public',
defaultElement: 'button',
role: 'button',
optional: false,
states: ['open', 'closed'],
data: [{ attr: 'data-state', values: ['open', 'closed'] }],
aria: [
{ 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,
states: ['open', 'closed'],
data: [
{ attr: 'data-state', values: ['open', 'closed'] },
{ attr: 'data-last-action',
values: ['saved', 'cancelled', 'dismissed', 'failed'],
severity: 'optional' }
],
aria: [
{
attr: 'aria-labelledby',
value: v.partRef('title'),
condition: { when: 'part-present', part: 'title' },
severity: 'recommended'
}
],
keyboard: [{ key: 'Escape', action: 'close' }]
},
{
name: 'Title',
kebab: 'title',
kind: 'public',
defaultElement: 'div',
role: 'heading',
optional: true,
data: [],
aria: [{ attr: 'aria-level', value: v.propRef('level'), severity: 'recommended' }]
}
]
};
```
---
## Authoring a new morfo
### Step 1 — Create the file
Write `src/uix/morfo/components/{kebab}.ts` exporting a `{camelName}Morfo` const.
**Naming rules**:
- `name`: PascalCase. `"Dialog"`, `"DateRangePicker"`, `"ColorField"`.
- `kebab`: kebab-case. `"dialog"`, `"date-range-picker"`, `"color-field"`. Must match `data-{kebab}` and `createAttrs({component})` in the provider.
- Part `kebab`s must be **unique across the whole morfo** — no nested path namespacing. If a conflict arises, rename (e.g. `item-trigger` instead of `trigger`).
- The root part uses `kebab: 'root'` — special-cased to emit `data-{component}` with no suffix.
### Step 2 — Declare parts
For each part, decide:
- **`kind`**:
- `'public'` when the consumer composes the part (e.g. `<Dialog.Trigger>`).
- `'virtual'` for internal coordinators that have no DOM of their own (context-only providers, focus guards). Use with `defaultElement: 'none'`.
- **`defaultElement`**: the HTML element the wrapper renders by default. Advisory — the consumer can override via `child` snippet. See the `MorfoElement` union in [`types.ts`](./types.ts).
- **`role`**: always-emitted ARIA role. Declare this even when the element has an implicit role (e.g. `<button>` has `role=button`) — this makes the contract polymorphism-safe: if the consumer uses `<div>` via `child`, the role still applies.
- **`optional`**: `true` if the part may be absent from a valid composition (Title, Description, Close, Overlay, Indicator, Separator). `false` for required parts (root + core).
- **`states`**: only declare if the part carries a `data-state` enum. List the exact values (e.g. `['open', 'closed']`). Required for `stateRef` ARIA values to validate.
- **`supportsNesting`**: `true` if this part can nest inside itself (Dialog inside Dialog, Menu inside Menu). Informational — enables eidos to style nested instances with scoped selectors.
### Step 3 — Declare `data` entries
For each data-attr the provider emits:
```ts
data: [
// Enum-valued: the complete set.
{ attr: 'data-state', values: ['open', 'closed'] },
// Presence-only flag: emitted only when true, absent otherwise.
{ attr: 'data-disabled', severity: 'optional' },
// Conditionally-emitted attr.
{
attr: 'data-starting-style',
severity: 'optional',
condition: { when: 'state-equals', state: 'open', value: 'starting' }
}
]
```
**Severity rules**:
- `'required'` (default) — the attr must always be emitted. Missing = error in strict mode.
- `'recommended'` — emitted in most cases. Missing = warning.
- `'optional'` — emitted only when its condition is met (e.g. `data-disabled` only when disabled). Missing ≠ error.
Use `'optional'` for all presence-only flags so morfo-check doesn't flag them as missing when they're legitimately absent.
### Step 4 — Declare `aria` entries
ARIA values use a tagged union via builders:
```ts
aria: [
{ attr: 'aria-haspopup', value: v.literal('dialog') }, // static literal
{ attr: 'aria-expanded', value: v.stateRef('open') }, // refs states[]
{ attr: 'aria-labelledby', value: v.partRef('title') }, // refs another part's kebab
{ attr: 'aria-level', value: v.propRef('level') }, // refs a consumer prop
{ attr: 'aria-label', value: v.translationRef('common.buttons.close') } // refs a langs.ts idlangref
]
```
**Value kinds**:
- `v.literal(value)` — static string invariant. `aria-haspopup="dialog"`, `aria-modal="true"`.
- `v.stateRef(name)` — the attribute value derives from a named state. Validator requires `name` to be in the containing part's `states[]`.
- `v.partRef(target)` — the attribute value is the id of another part. Validator requires `target` to be an existing kebab in the morfo.
- `v.propRef(prop)` — the attribute value comes from a consumer prop (override, passthrough).
- `v.translationRef(key)` — the attribute value resolves to a translation.
Add `condition` when the ARIA is emitted only in some cases:
```ts
condition: 'always'
condition: { when: 'part-present', part: 'title' }
condition: { when: 'state-equals', state: 'open', value: 'true' }
condition: { when: 'prop-truthy', prop: 'modal' }
condition: { when: 'prop-falsy', prop: 'disabled' }
```
### Step 5 — Optional: keyboard and focus
```ts
keyboard: [
{ key: 'Escape', action: 'close' },
{ key: 'Tab', action: 'focus-next' },
{ key: 'Shift+Tab', action: 'focus-prev' }
]
focus: {
initial: 'first-focusable', // 'first-focusable' | 'trigger' | { partRef: 'content' }
trap: true,
return: 'trigger', // 'trigger' | 'previous' | { partRef: '...' }
restore: true
}
```
Focus is only required for overlay/composite components (Dialog, Drawer, Popover). Plain controls omit it.
### Step 6 — Wire the provider
In `src/uix/soma/components/{kebab}/{kebab}-provider.svelte.ts`:
```ts
import { createAttrs, registerContract } from '../../attrs';
import { dialogMorfo } from '../../../morfo/components/dialog';
const attrs = createAttrs(dialogMorfo);
registerContract(dialogMorfo);
```
That's it. No more inline `createAttrs({component, parts})`, no more `registerContract({name, version, parts: {...}})`.
---
## Validation
Three layers catch three classes of drift:
### 1. Sium schema (build / dev)
`src/uix/morfo/schema.ts` exports `validateMorfo(morfo)` which:
- Checks shape: types, enum discriminators, literal unions.
- Checks cross-field invariants:
- Every part `kebab` is unique in the morfo.
- Every `partRef.target` resolves to an existing part.
- Every `stateRef.state` exists in the containing part's `states[]`.
- Every `state-equals` condition's `state` exists in the containing part.
- `focus.initial.partRef` / `focus.return.partRef` resolve.
- `scope` is non-empty.
- `kebab` matches `/^[a-z][a-z0-9-]*$/`.
Run a morfo through this to catch authoring errors early. See [`components/dialog.test.ts`](./components/dialog.test.ts) for a reference test.
### 2. Strict mode in `assertContract` (dev runtime)
The provider's `assertProps` walks the emitted data-attrs and checks their values against the registered contract. In dev mode, a value not in the declared `values[]` logs a warning:
```
[soma] dialog.content: "data-state" has value "opening" but contract expects one of: open, closed
```
### 3. Smoke + morfo-check (CI)
Two npm scripts exercise the morfos against the real DOM:
- `npm run smoke` — Playwright walks all `/test/soma/*` routes. Catches `pageerror`, `console.error`, translation-key-not-found, context-not-found. Not morfo-specific but catches common regressions.
- `npm run morfo:check` — For each morfo, navigates to `/test/soma/{kebab}` and validates:
- Every declared data-attr with `severity: 'required'` is emitted.
- Every emitted data-attr value matches `values[]` if declared.
- No undeclared `data-{component}-*` attrs are emitted (except `data-_*` private).
- `npm run morfo:vocabulary` — Flags `data-state` enums that diverge from canonical vocabularies (`open|closed`, `active|inactive`, `checked|unchecked|indeterminate`, etc.). WARN-level; novel vocabularies may be legitimate but should be reviewed.
Both scripts require `npm run dev` running in another terminal.
---
## The data-attr convention
`createAttrs(morfo)` derives data-attr names from parts:
| Part kebab | Emitted attr |
|------------|--------------|
| `'root'` | `data-{component}` — no suffix |
| `'trigger'` | `data-{component}-trigger` |
| `'item-group'` | `data-{component}-item-group` |
Never `data-soma-*`, never `data-eidos-*` — always `data-{component}[-{part}]`.
Private attrs for internal debug / state use the `data-_*` prefix (convention only — no enforcement), which is skipped by strict-mode validation.
---
## Handling polymorphism (consumer renders a different element)
The `child` snippet pattern allows consumers to swap the default element:
```svelte
<Dialog.Trigger>
{#snippet child({ props })}
<a href="/about" {...props}>About</a>
{/snippet}
</Dialog.Trigger>
```
Morfo's `defaultElement` is **advisory** — the provider doesn't enforce it. What IS guaranteed is `role`: the provider always emits the explicit role (e.g. `role="button"` on a Trigger even though `<button>` has it implicitly). When the consumer renders as `<a>`, the role stays correct.
Keyboard handlers should also be element-agnostic: emit `onkeydown` that handles both Enter and Space for "activate" regardless of the underlying element, since `<a>` only handles Enter natively and `<div>` handles neither.
---
## Sema alignment
[Sema](../sema/sema_pre.md) is the future perceptual/semantic layer. It consumes the same DOM surface that morfo declares — no extra hooks needed. The morfo authoring rules that support Sema:
- **Transition markers**: components with enter/exit transitions declare `data-starting-style` and `data-ending-style` on the transitioning part.
- **Causal exit states**: components with multiple semantically distinct exit paths (Dialog: saved / cancelled / dismissed / failed; Toast: dismissed / auto-timeout / action) declare `data-last-action` with enumerable `values`. The provider is expected to update `data-last-action` **before** `data-state` changes, so Sema can tint the exit animation per-action. (Tracked by a dedicated MutationObserver timing test — future work.)
- **Cross-component vocabulary consistency**: the `morfo:vocabulary` script groups components by data-attr semantic (disclosure → `open|closed`, lifecycle → `loading|idle|success|error`) and flags divergent vocabularies for review.
These don't change the morfo shape — they're authoring conventions that enable Sema without requiring a Sema-aware provider.
---
## Files in this package
```
src/uix/morfo/
├── README.md ← (this file) developer guide
├── DESIGN.md ← design proposal shared with external reviewers
├── study.md ← record of the iterative AI peer-review process
├── types.ts ← Morfo interface + v.* builders
├── schema.ts ← sium-based validator + CANONICAL_VOCABULARIES
├── index.ts ← package barrel
└── components/
├── dialog.ts ← one morfo per component (66 files)
├── accordion.ts
├── ...
└── dialog.test.ts ← reference test pattern
```
---
## Commands
| Command | Purpose |
|---------|---------|
| `npm run check` | TypeScript type-check across the repo (catches shape errors in morfos). |
| `npx vitest run src/uix/morfo` | Run morfo unit tests (schema invariants). |
| `npm run smoke` | Playwright smoke over all `/test/soma/*` routes (requires dev server). |
| `npm run morfo:check` | Validate emitted DOM vs morfo per-component (requires dev server). |
| `npm run morfo:vocabulary` | Flag data-state enums that diverge from canonical vocabularies. |
---
## Common pitfalls
**Forgetting `as const` or a type annotation.** Morfo files should declare `satisfies Morfo` or `: Morfo` so TypeScript catches shape errors early:
```ts
// Good
export const dialogMorfo: Morfo = { ... };
// Also good
export const dialogMorfo = { ... } satisfies Morfo;
```
**Duplicate kebab in the tree.** `item` in one part and `item` in another = error. Rename one.
**`stateRef` without declaring `states[]`.** If a part emits `aria-expanded` via `v.stateRef('open')`, the part **must** declare `states: ['open', ...]`. Otherwise the validator throws.
**`partRef` to a non-existent kebab.** Common after renaming a part. The validator catches this — but the dev-time warning is silent if you skip `validateMorfo`.
**Missing `severity: 'optional'` on presence flags.** If you declare `{ attr: 'data-disabled' }` without severity, strict mode treats it as required. Add `severity: 'optional'` so morfo-check doesn't flag it missing when the flag is legitimately absent.
**Provider emits a data-attr not in the morfo.** Strict mode logs a warning at runtime; morfo-check fails in CI. Either add the attr to the morfo or rename the provider's emission to `data-_*` (private).
---
## See also
- [DESIGN.md](./DESIGN.md) — the original proposal (for context on why the shape is what it is).
- [study.md](./study.md) — the three-round AI peer-review process that refined the shape.
- [types.ts](./types.ts) — the TypeScript interfaces (authoritative reference).
- [COMPONENT_GUIDE.md](../soma/COMPONENT_GUIDE.md) — soma component authoring (includes items 37-39 on morfo-specific discipline: translation-namespace grep, DOM-topology audit, smoke validation).
- [sema_pre.md](../sema/sema_pre.md) — semantic layer context (morfo provides everything Sema needs).

@ -1,4 +1,5 @@
import type { Morfo } from '../types';
import { v } from '../types';
export const accordionMorfo: Morfo = {
name: 'Accordion',
@ -37,6 +38,7 @@ export const accordionMorfo: Morfo = {
kebab: 'header',
kind: 'public',
defaultElement: 'div',
role: 'heading',
optional: false,
states: ['open', 'closed'],
data: [
@ -44,13 +46,14 @@ export const accordionMorfo: Morfo = {
{ attr: 'data-disabled', severity: 'optional' },
{ attr: 'data-orientation', values: ['horizontal', 'vertical'] }
],
aria: []
aria: [{ attr: 'aria-level', value: v.propRef('level') }]
},
{
name: 'Trigger',
kebab: 'trigger',
kind: 'public',
defaultElement: 'button',
role: 'button',
optional: false,
states: ['open', 'closed'],
data: [
@ -58,13 +61,31 @@ export const accordionMorfo: Morfo = {
{ attr: 'data-disabled', severity: 'optional' },
{ attr: 'data-orientation', values: ['horizontal', 'vertical'] }
],
aria: []
aria: [
{ attr: 'type', value: v.literal('button') },
{ attr: 'aria-expanded', value: v.stateRef('open') },
{
attr: 'aria-controls',
value: v.partRef('content'),
condition: { when: 'part-present', part: 'content' },
severity: 'recommended'
}
],
keyboard: [
{ key: 'ArrowDown', action: 'next-item' },
{ key: 'ArrowUp', action: 'prev-item' },
{ key: 'ArrowRight', action: 'next-item' },
{ key: 'ArrowLeft', action: 'prev-item' },
{ key: 'Home', action: 'first-item' },
{ key: 'End', action: 'last-item' }
]
},
{
name: 'Content',
kebab: 'content',
kind: 'public',
defaultElement: 'div',
role: 'region',
optional: false,
states: ['open', 'closed'],
data: [
@ -74,7 +95,14 @@ export const accordionMorfo: Morfo = {
{ attr: 'data-starting-style', severity: 'optional' },
{ attr: 'data-ending-style', severity: 'optional' }
],
aria: []
aria: [
{
attr: 'aria-labelledby',
value: v.partRef('trigger'),
condition: { when: 'part-present', part: 'trigger' },
severity: 'recommended'
}
]
}
]
};

@ -1,10 +1,19 @@
import type { Morfo } from '../types';
import { v } from '../types';
export const alertDialogMorfo: Morfo = {
name: 'AlertDialog',
kebab: 'alert-dialog',
scope: ['soma'],
apg: 'https://www.w3.org/WAI/ARIA/apg/patterns/alertdialog/',
focus: {
initial: 'first-focusable',
trap: true,
return: 'trigger',
restore: true
},
parts: [
{
name: 'Provider',
@ -23,7 +32,14 @@ export const alertDialogMorfo: Morfo = {
role: 'button',
optional: false,
data: [],
aria: []
aria: [
{ attr: 'type', value: v.literal('button') },
{
attr: 'aria-label',
value: v.translationRef('#?components.alert-dialog.action|Confirm'),
severity: 'recommended'
}
]
},
{
name: 'Cancel',
@ -33,7 +49,14 @@ export const alertDialogMorfo: Morfo = {
role: 'button',
optional: false,
data: [],
aria: []
aria: [
{ attr: 'type', value: v.literal('button') },
{
attr: 'aria-label',
value: v.translationRef('#?components.alert-dialog.cancel|Cancel'),
severity: 'recommended'
}
]
}
]
};

@ -1,4 +1,5 @@
import type { Morfo } from '../types';
import { v } from '../types';
export const announceMorfo: Morfo = {
name: 'Announce',
@ -24,7 +25,12 @@ export const announceMorfo: Morfo = {
{ attr: 'data-role', values: ['status', 'alert', 'log', 'timer'] },
{ attr: 'data-live', values: ['polite', 'assertive'] }
],
aria: []
aria: [
{ attr: 'role', value: v.propRef('role') },
{ attr: 'aria-live', value: v.propRef('ariaLive') },
{ attr: 'aria-atomic', value: v.propRef('ariaAtomic') },
{ attr: 'aria-relevant', value: v.propRef('ariaRelevant') }
]
}
]
};

@ -1,4 +1,5 @@
import type { Morfo } from '../types';
import { v } from '../types';
export const breadcrumbMorfo: Morfo = {
name: 'Breadcrumb',
@ -13,7 +14,13 @@ export const breadcrumbMorfo: Morfo = {
defaultElement: 'nav',
optional: false,
data: [],
aria: []
aria: [
{
attr: 'aria-label',
value: v.translationRef('#?components.breadcrumb.label|Breadcrumb'),
severity: 'recommended'
}
]
},
{
name: 'List',
@ -40,16 +47,24 @@ export const breadcrumbMorfo: Morfo = {
defaultElement: 'a',
optional: false,
data: [{ attr: 'data-current', severity: 'optional' }],
aria: []
aria: [
{
attr: 'aria-current',
value: v.propRef('current'),
severity: 'optional',
condition: { when: 'prop-truthy', prop: 'current' }
}
]
},
{
name: 'Separator',
kebab: 'separator',
kind: 'public',
defaultElement: 'li',
role: 'presentation',
optional: true,
data: [],
aria: []
aria: [{ attr: 'aria-hidden', value: v.literal('true') }]
},
{
name: 'Ellipsis',
@ -58,7 +73,19 @@ export const breadcrumbMorfo: Morfo = {
defaultElement: 'span',
optional: true,
data: [{ attr: 'data-interactive', severity: 'optional' }],
aria: []
aria: [
{
attr: 'aria-label',
value: v.translationRef('#?components.breadcrumb.ellipsis|More'),
severity: 'recommended'
},
{
attr: 'aria-haspopup',
value: v.propRef('popupType'),
severity: 'optional',
condition: { when: 'prop-truthy', prop: 'interactive' }
}
]
}
]
};

@ -1,4 +1,5 @@
import type { Morfo } from '../types';
import { v } from '../types';
export const calendarMorfo: Morfo = {
name: 'Calendar',
@ -11,6 +12,7 @@ export const calendarMorfo: Morfo = {
kebab: 'root',
kind: 'public',
defaultElement: 'div',
role: 'application',
optional: false,
data: [
{ attr: 'data-type', severity: 'optional' },
@ -18,27 +20,76 @@ export const calendarMorfo: Morfo = {
{ attr: 'data-disabled', severity: 'optional' },
{ attr: 'data-invalid', severity: 'optional' }
],
aria: [
{ attr: 'aria-label', value: v.propRef('ariaLabel'), severity: 'recommended' },
{ attr: 'aria-disabled', value: v.propRef('disabled'), severity: 'optional' }
],
keyboard: [
{ key: 'ArrowRight', action: 'next-day' },
{ key: 'ArrowLeft', action: 'prev-day' },
{ key: 'ArrowDown', action: 'next-week' },
{ key: 'ArrowUp', action: 'prev-week' },
{ key: 'PageUp', action: 'prev-month' },
{ key: 'PageDown', action: 'next-month' },
{ key: 'Home', action: 'first-day-of-week' },
{ key: 'End', action: 'last-day-of-week' },
{ key: 'Enter', action: 'select' },
{ key: ' ', action: 'select' }
]
},
{
name: 'Header',
kebab: 'header',
kind: 'public',
defaultElement: 'div',
optional: true,
data: [],
aria: [{ attr: 'aria-hidden', value: v.literal('true') }]
},
{
name: 'Heading',
kebab: 'heading',
kind: 'public',
defaultElement: 'div',
optional: true,
data: [],
aria: []
},
{ name: 'Header', kebab: 'header', kind: 'public', defaultElement: 'div', optional: true, data: [], aria: [] },
{ name: 'Heading', kebab: 'heading', kind: 'public', defaultElement: 'div', optional: true, data: [], aria: [] },
{
name: 'PrevButton',
kebab: 'prev-button',
kind: 'public',
defaultElement: 'button',
role: 'button',
optional: true,
data: [{ attr: 'data-disabled', severity: 'optional' }],
aria: []
aria: [
{ attr: 'type', value: v.literal('button') },
{
attr: 'aria-label',
value: v.translationRef('#?components.calendar.prev-month|Previous month'),
severity: 'recommended'
},
{ attr: 'aria-disabled', value: v.propRef('disabled'), severity: 'optional' }
]
},
{
name: 'NextButton',
kebab: 'next-button',
kind: 'public',
defaultElement: 'button',
role: 'button',
optional: true,
data: [{ attr: 'data-disabled', severity: 'optional' }],
aria: []
aria: [
{ attr: 'type', value: v.literal('button') },
{
attr: 'aria-label',
value: v.translationRef('#?components.calendar.next-month|Next month'),
severity: 'recommended'
},
{ attr: 'aria-disabled', value: v.propRef('disabled'), severity: 'optional' }
]
},
{
name: 'MonthSelect',
@ -47,7 +98,14 @@ export const calendarMorfo: Morfo = {
defaultElement: 'select',
optional: true,
data: [{ attr: 'data-disabled', severity: 'optional' }],
aria: []
aria: [
{
attr: 'aria-label',
value: v.translationRef('#?components.calendar.month|Month'),
severity: 'recommended'
},
{ attr: 'aria-disabled', value: v.propRef('disabled'), severity: 'optional' }
]
},
{
name: 'YearSelect',
@ -56,18 +114,77 @@ export const calendarMorfo: Morfo = {
defaultElement: 'select',
optional: true,
data: [{ attr: 'data-disabled', severity: 'optional' }],
aria: [
{
attr: 'aria-label',
value: v.translationRef('#?components.calendar.year|Year'),
severity: 'recommended'
},
{ attr: 'aria-disabled', value: v.propRef('disabled'), severity: 'optional' }
]
},
{
name: 'Grid',
kebab: 'grid',
kind: 'public',
defaultElement: 'table',
role: 'grid',
optional: false,
data: [],
aria: [
{
attr: 'aria-labelledby',
value: v.partRef('heading'),
condition: { when: 'part-present', part: 'heading' },
severity: 'recommended'
},
{ attr: 'aria-readonly', value: v.propRef('readonly'), severity: 'optional' },
{ attr: 'aria-disabled', value: v.propRef('disabled'), severity: 'optional' }
]
},
{
name: 'GridHead',
kebab: 'grid-head',
kind: 'public',
defaultElement: 'thead',
optional: false,
data: [],
aria: []
},
{
name: 'GridBody',
kebab: 'grid-body',
kind: 'public',
defaultElement: 'tbody',
optional: false,
data: [],
aria: []
},
{
name: 'GridRow',
kebab: 'grid-row',
kind: 'public',
defaultElement: 'tr',
role: 'row',
optional: false,
data: [],
aria: []
},
{
name: 'HeadCell',
kebab: 'head-cell',
kind: 'public',
defaultElement: 'th',
optional: false,
data: [],
aria: []
},
{ name: 'Grid', kebab: 'grid', kind: 'public', defaultElement: 'table', optional: false, data: [], aria: [] },
{ name: 'GridHead', kebab: 'grid-head', kind: 'public', defaultElement: 'thead', optional: false, data: [], aria: [] },
{ name: 'GridBody', kebab: 'grid-body', kind: 'public', defaultElement: 'tbody', optional: false, data: [], aria: [] },
{ name: 'GridRow', kebab: 'grid-row', kind: 'public', defaultElement: 'tr', optional: false, data: [], aria: [] },
{ name: 'HeadCell', kebab: 'head-cell', kind: 'public', defaultElement: 'th', optional: false, data: [], aria: [] },
{
name: 'Cell',
kebab: 'cell',
kind: 'public',
defaultElement: 'td',
role: 'gridcell',
optional: false,
data: [
{ attr: 'data-selected', severity: 'optional' },
@ -78,13 +195,17 @@ export const calendarMorfo: Morfo = {
{ attr: 'data-holiday', severity: 'optional' },
{ attr: 'data-outside-month', severity: 'optional' }
],
aria: []
aria: [
{ attr: 'aria-selected', value: v.propRef('selected'), severity: 'optional' },
{ attr: 'aria-disabled', value: v.propRef('disabled'), severity: 'optional' }
]
},
{
name: 'Day',
kebab: 'day',
kind: 'public',
defaultElement: 'div',
role: 'button',
optional: false,
data: [
{ attr: 'data-selected', severity: 'optional' },
@ -96,7 +217,11 @@ export const calendarMorfo: Morfo = {
{ attr: 'data-outside-month', severity: 'optional' },
{ attr: 'data-focused', severity: 'optional' }
],
aria: []
aria: [
{ attr: 'aria-label', value: v.propRef('dateLabel'), severity: 'recommended' },
{ attr: 'aria-disabled', value: v.propRef('disabled'), severity: 'optional' },
{ attr: 'aria-selected', value: v.propRef('selected'), severity: 'optional' }
]
},
{
name: 'WeekNumber',

@ -1,4 +1,5 @@
import type { Morfo } from '../types';
import { v } from '../types';
export const carouselMorfo: Morfo = {
name: 'Carousel',
@ -11,6 +12,7 @@ export const carouselMorfo: Morfo = {
kebab: 'root',
kind: 'public',
defaultElement: 'section',
role: 'region',
optional: false,
data: [
{ attr: 'data-orientation', values: ['horizontal', 'vertical'] },
@ -18,7 +20,22 @@ export const carouselMorfo: Morfo = {
{ attr: 'data-dragging', severity: 'optional' },
{ attr: 'data-autoplay', severity: 'optional' }
],
aria: []
aria: [
{ attr: 'aria-roledescription', value: v.literal('carousel') },
{
attr: 'aria-label',
value: v.translationRef('#?components.carousel.label|Carousel'),
severity: 'recommended'
}
],
keyboard: [
{ key: 'ArrowRight', action: 'next-item' },
{ key: 'ArrowLeft', action: 'prev-item' },
{ key: 'ArrowDown', action: 'next-item' },
{ key: 'ArrowUp', action: 'prev-item' },
{ key: 'Home', action: 'first-item' },
{ key: 'End', action: 'last-item' }
]
},
{
name: 'Viewport',
@ -46,6 +63,7 @@ export const carouselMorfo: Morfo = {
kebab: 'item',
kind: 'public',
defaultElement: 'div',
role: 'group',
optional: false,
states: ['active', 'inactive'],
data: [
@ -53,47 +71,72 @@ export const carouselMorfo: Morfo = {
{ attr: 'data-index' },
{ attr: 'data-orientation', values: ['horizontal', 'vertical'] }
],
aria: []
aria: [
{ attr: 'aria-roledescription', value: v.literal('slide') },
{ attr: 'aria-label', value: v.propRef('index'), severity: 'recommended' }
]
},
{
name: 'PrevTrigger',
kebab: 'prev-trigger',
kind: 'public',
defaultElement: 'button',
role: 'button',
optional: true,
data: [{ attr: 'data-disabled', severity: 'optional' }],
aria: []
aria: [
{ attr: 'type', value: v.literal('button') },
{
attr: 'aria-label',
value: v.translationRef('#?components.carousel.prev|Previous slide'),
severity: 'recommended'
}
]
},
{
name: 'NextTrigger',
kebab: 'next-trigger',
kind: 'public',
defaultElement: 'button',
role: 'button',
optional: true,
data: [{ attr: 'data-disabled', severity: 'optional' }],
aria: []
aria: [
{ attr: 'type', value: v.literal('button') },
{
attr: 'aria-label',
value: v.translationRef('#?components.carousel.next|Next slide'),
severity: 'recommended'
}
]
},
{
name: 'IndicatorGroup',
kebab: 'indicator-group',
kind: 'public',
defaultElement: 'div',
role: 'tablist',
optional: true,
data: [{ attr: 'data-orientation', values: ['horizontal', 'vertical'] }],
aria: []
aria: [{ attr: 'aria-orientation', value: v.propRef('orientation') }]
},
{
name: 'Indicator',
kebab: 'indicator',
kind: 'public',
defaultElement: 'button',
role: 'tab',
optional: true,
states: ['active', 'inactive'],
data: [
{ attr: 'data-state', values: ['active', 'inactive'] },
{ attr: 'data-index' }
],
aria: []
aria: [
{ attr: 'type', value: v.literal('button') },
{ attr: 'aria-selected', value: v.stateRef('active') },
{ attr: 'aria-label', value: v.propRef('ariaLabel'), severity: 'recommended' }
]
}
]
};

@ -1,4 +1,5 @@
import type { Morfo } from '../types';
import { v } from '../types';
export const checkboxMorfo: Morfo = {
name: 'Checkbox',
@ -19,16 +20,40 @@ export const checkboxMorfo: Morfo = {
{ attr: 'data-disabled', severity: 'optional' },
{ attr: 'data-readonly', severity: 'optional' }
],
aria: []
aria: [
{ attr: 'type', value: v.literal('button') },
{ attr: 'aria-checked', value: v.stateRef('checked') },
{
attr: 'aria-required',
value: v.literal('true'),
severity: 'optional',
condition: { when: 'prop-truthy', prop: 'required' }
},
{
attr: 'aria-readonly',
value: v.literal('true'),
severity: 'optional',
condition: { when: 'prop-truthy', prop: 'readonly' }
}
],
keyboard: [{ key: ' ', action: 'toggle' }]
},
{
name: 'Group',
kebab: 'group',
kind: 'public',
defaultElement: 'div',
role: 'group',
optional: true,
data: [{ attr: 'data-disabled', severity: 'optional' }],
aria: []
aria: [
{
attr: 'aria-labelledby',
value: v.partRef('group-label'),
condition: { when: 'part-present', part: 'group-label' },
severity: 'recommended'
}
]
},
{
name: 'GroupLabel',

@ -1,4 +1,5 @@
import type { Morfo } from '../types';
import { v } from '../types';
export const clipboardMorfo: Morfo = {
name: 'Clipboard',
@ -22,7 +23,15 @@ export const clipboardMorfo: Morfo = {
role: 'button',
optional: false,
data: [{ attr: 'data-copied', severity: 'optional' }],
aria: []
aria: [
{ attr: 'type', value: v.literal('button') },
{
attr: 'aria-label',
value: v.translationRef('#?components.clipboard.copy|Copy to clipboard'),
severity: 'recommended'
},
{ attr: 'aria-live', value: v.literal('polite') }
]
},
{
name: 'Indicator',
@ -31,7 +40,7 @@ export const clipboardMorfo: Morfo = {
defaultElement: 'span',
optional: true,
data: [{ attr: 'data-copied' }],
aria: []
aria: [{ attr: 'aria-hidden', value: v.literal('true') }]
}
]
};

@ -1,4 +1,5 @@
import type { Morfo } from '../types';
import { v } from '../types';
export const collapsibleMorfo: Morfo = {
name: 'Collapsible',
@ -24,26 +25,44 @@ export const collapsibleMorfo: Morfo = {
kebab: 'trigger',
kind: 'public',
defaultElement: 'button',
role: 'button',
optional: false,
states: ['open', 'closed'],
data: [
{ attr: 'data-state', values: ['open', 'closed'] },
{ attr: 'data-disabled', severity: 'optional' }
],
aria: []
aria: [
{ attr: 'type', value: v.literal('button') },
{ attr: 'aria-expanded', value: v.stateRef('open') },
{
attr: 'aria-controls',
value: v.partRef('content'),
condition: { when: 'part-present', part: 'content' },
severity: 'recommended'
}
]
},
{
name: 'Content',
kebab: 'content',
kind: 'public',
defaultElement: 'div',
role: 'region',
optional: false,
states: ['open', 'closed'],
data: [
{ attr: 'data-state', values: ['open', 'closed'] },
{ attr: 'data-disabled', severity: 'optional' }
],
aria: []
aria: [
{
attr: 'aria-labelledby',
value: v.partRef('trigger'),
condition: { when: 'part-present', part: 'trigger' },
severity: 'recommended'
}
]
}
]
};

@ -1,4 +1,5 @@
import type { Morfo } from '../types';
import { v } from '../types';
export const colorFieldMorfo: Morfo = {
name: 'ColorField',
@ -18,7 +19,14 @@ export const colorFieldMorfo: Morfo = {
{ attr: 'data-required', severity: 'optional' },
{ attr: 'data-format', values: ['hex', 'rgb', 'hsl'] }
],
aria: []
aria: [
{
attr: 'aria-invalid',
value: v.literal('true'),
severity: 'optional',
condition: { when: 'prop-truthy', prop: 'invalid' }
}
]
},
{
name: 'Label',
@ -37,19 +45,42 @@ export const colorFieldMorfo: Morfo = {
kebab: 'input',
kind: 'public',
defaultElement: 'div',
role: 'group',
optional: false,
data: [
{ attr: 'data-invalid', severity: 'optional' },
{ attr: 'data-disabled', severity: 'optional' },
{ attr: 'data-format', values: ['hex', 'rgb', 'hsl'] }
],
aria: []
aria: [
{
attr: 'aria-label',
value: v.propRef('ariaLabel'),
severity: 'optional',
condition: { when: 'prop-truthy', prop: 'ariaLabel' }
},
{
attr: 'aria-labelledby',
value: v.partRef('label'),
severity: 'optional',
condition: { when: 'part-present', part: 'label' }
},
{ attr: 'aria-disabled', value: v.propRef('disabled'), severity: 'optional' },
{ attr: 'aria-readonly', value: v.propRef('readonly'), severity: 'optional' },
{
attr: 'aria-invalid',
value: v.literal('true'),
severity: 'optional',
condition: { when: 'prop-truthy', prop: 'invalid' }
}
]
},
{
name: 'Segment',
kebab: 'segment',
kind: 'public',
defaultElement: 'span',
role: 'spinbutton',
optional: false,
data: [
{ attr: 'data-invalid', severity: 'optional' },
@ -58,7 +89,30 @@ export const colorFieldMorfo: Morfo = {
{ attr: 'data-placeholder', severity: 'optional' },
{ attr: 'data-segment' }
],
aria: []
aria: [
{ attr: 'aria-label', value: v.propRef('segmentLabel') },
{ attr: 'aria-valuemin', value: v.propRef('min') },
{ attr: 'aria-valuemax', value: v.propRef('max') },
{ attr: 'aria-valuenow', value: v.propRef('value') },
{ attr: 'aria-valuetext', value: v.propRef('value') },
{ attr: 'aria-disabled', value: v.propRef('disabled'), severity: 'optional' },
{ attr: 'aria-readonly', value: v.propRef('readonly'), severity: 'optional' },
{
attr: 'aria-invalid',
value: v.literal('true'),
severity: 'optional',
condition: { when: 'prop-truthy', prop: 'invalid' }
}
],
keyboard: [
{ key: 'ArrowUp', action: 'increment' },
{ key: 'ArrowDown', action: 'decrement' },
{ key: 'Home', action: 'first-item' },
{ key: 'End', action: 'last-item' },
{ key: 'Backspace', action: 'delete' },
{ key: 'ArrowLeft', action: 'prev-segment' },
{ key: 'ArrowRight', action: 'next-segment' }
]
},
{
name: 'FormatSelect',
@ -70,7 +124,13 @@ export const colorFieldMorfo: Morfo = {
{ attr: 'data-disabled', severity: 'optional' },
{ attr: 'data-locked', severity: 'optional' }
],
aria: []
aria: [
{
attr: 'aria-label',
value: v.translationRef('#?components.color-field.format-select|Color format'),
severity: 'recommended'
}
]
}
]
};

@ -1,9 +1,18 @@
import type { Morfo } from '../types';
import { v } from '../types';
export const colorPickerMorfo: Morfo = {
name: 'ColorPicker',
kebab: 'color-picker',
scope: ['soma'],
focus: {
initial: 'first-focusable',
trap: true,
return: 'trigger',
restore: true
},
parts: [
{
name: 'Provider',
@ -46,21 +55,54 @@ export const colorPickerMorfo: Morfo = {
kebab: 'trigger',
kind: 'public',
defaultElement: 'button',
role: 'button',
optional: true,
states: ['open', 'closed'],
data: [
{ attr: 'data-state', values: ['open', 'closed'] },
{ attr: 'data-disabled', severity: 'optional' }
],
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'),
condition: { when: 'part-present', part: 'content' },
severity: 'recommended'
},
{
attr: 'aria-label',
value: v.translationRef('#?components.color-picker.trigger|Choose color'),
severity: 'recommended'
}
]
},
{
name: 'ValueSwatch',
kebab: 'value-swatch',
kind: 'public',
defaultElement: 'div',
optional: true,
data: [],
aria: [{ attr: 'aria-hidden', value: v.literal('true') }]
},
{
name: 'ValueText',
kebab: 'value-text',
kind: 'public',
defaultElement: 'span',
optional: true,
data: [],
aria: []
},
{ name: 'ValueSwatch', kebab: 'value-swatch', kind: 'public', defaultElement: 'div', optional: true, data: [], aria: [] },
{ name: 'ValueText', kebab: 'value-text', kind: 'public', defaultElement: 'span', optional: true, data: [], aria: [] },
{
name: 'Content',
kebab: 'content',
kind: 'public',
defaultElement: 'div',
role: 'dialog',
optional: true,
states: ['open', 'closed'],
data: [{ attr: 'data-state', values: ['open', 'closed'] }],
@ -79,15 +121,27 @@ export const colorPickerMorfo: Morfo = {
],
aria: []
},
{ name: 'AreaBackground', kebab: 'area-background', kind: 'public', defaultElement: 'div', optional: true, data: [], aria: [] },
{
name: 'AreaBackground',
kebab: 'area-background',
kind: 'public',
defaultElement: 'div',
optional: true,
data: [],
aria: [{ attr: 'aria-hidden', value: v.literal('true') }]
},
{
name: 'AreaThumb',
kebab: 'area-thumb',
kind: 'public',
defaultElement: 'div',
role: 'slider',
optional: true,
data: [{ attr: 'data-dragging', severity: 'optional' }],
aria: []
aria: [
{ attr: 'aria-label', value: v.propRef('ariaLabel'), severity: 'recommended' },
{ attr: 'aria-valuenow', value: v.propRef('value') }
]
},
{
name: 'ChannelSlider',
@ -102,18 +156,46 @@ export const colorPickerMorfo: Morfo = {
],
aria: []
},
{ name: 'ChannelSliderTrack', kebab: 'channel-slider-track', kind: 'public', defaultElement: 'div', optional: true, data: [], aria: [] },
{
name: 'ChannelSliderTrack',
kebab: 'channel-slider-track',
kind: 'public',
defaultElement: 'div',
optional: true,
data: [],
aria: []
},
{
name: 'ChannelSliderThumb',
kebab: 'channel-slider-thumb',
kind: 'public',
defaultElement: 'div',
role: 'slider',
optional: true,
data: [{ attr: 'data-dragging', severity: 'optional' }],
aria: [
{ attr: 'aria-label', value: v.propRef('ariaLabel'), severity: 'recommended' },
{ attr: 'aria-valuenow', value: v.propRef('value') }
]
},
{
name: 'ChannelInput',
kebab: 'channel-input',
kind: 'public',
defaultElement: 'input',
optional: true,
data: [],
aria: [{ attr: 'aria-label', value: v.propRef('channelLabel'), severity: 'recommended' }]
},
{
name: 'ChannelSegment',
kebab: 'channel-segment',
kind: 'public',
defaultElement: 'span',
optional: true,
data: [],
aria: []
},
{ name: 'ChannelInput', kebab: 'channel-input', kind: 'public', defaultElement: 'input', optional: true, data: [], aria: [] },
{ name: 'ChannelSegment', kebab: 'channel-segment', kind: 'public', defaultElement: 'span', optional: true, data: [], aria: [] },
{
name: 'FormatSelect',
kebab: 'format-select',
@ -121,23 +203,46 @@ export const colorPickerMorfo: Morfo = {
defaultElement: 'select',
optional: true,
data: [{ attr: 'data-locked', severity: 'optional' }],
aria: []
aria: [
{
attr: 'aria-label',
value: v.translationRef('#?components.color-picker.format-select|Color format'),
severity: 'recommended'
}
]
},
{
name: 'TransparencyGrid',
kebab: 'transparency-grid',
kind: 'public',
defaultElement: 'div',
optional: true,
data: [],
aria: [{ attr: 'aria-hidden', value: v.literal('true') }]
},
{ name: 'TransparencyGrid', kebab: 'transparency-grid', kind: 'public', defaultElement: 'div', optional: true, data: [], aria: [] },
{
name: 'EyeDropper',
kebab: 'eye-dropper',
kind: 'public',
defaultElement: 'button',
role: 'button',
optional: true,
data: [{ attr: 'data-unsupported', severity: 'optional' }],
aria: []
aria: [
{ attr: 'type', value: v.literal('button') },
{
attr: 'aria-label',
value: v.translationRef('#?components.color-picker.eye-dropper|Pick color from screen'),
severity: 'recommended'
}
]
},
{
name: 'SwatchGroup',
kebab: 'swatch-group',
kind: 'public',
defaultElement: 'div',
role: 'group',
optional: true,
data: [{ attr: 'data-disabled', severity: 'optional' }],
aria: []
@ -147,14 +252,32 @@ export const colorPickerMorfo: Morfo = {
kebab: 'swatch-trigger',
kind: 'public',
defaultElement: 'button',
role: 'button',
optional: true,
data: [
{ attr: 'data-checked', severity: 'optional' },
{ attr: 'data-disabled', severity: 'optional' }
],
aria: []
aria: [
{ attr: 'type', value: v.literal('button') },
{ attr: 'aria-label', value: v.propRef('swatchLabel'), severity: 'recommended' },
{
attr: 'aria-checked',
value: v.literal('true'),
severity: 'optional',
condition: { when: 'prop-truthy', prop: 'checked' }
}
]
},
{
name: 'Swatch',
kebab: 'swatch',
kind: 'public',
defaultElement: 'div',
optional: true,
data: [],
aria: [{ attr: 'aria-hidden', value: v.literal('true') }]
},
{ name: 'Swatch', kebab: 'swatch', kind: 'public', defaultElement: 'div', optional: true, data: [], aria: [] },
{
name: 'SwatchIndicator',
kebab: 'swatch-indicator',
@ -162,7 +285,7 @@ export const colorPickerMorfo: Morfo = {
defaultElement: 'span',
optional: true,
data: [{ attr: 'data-checked', severity: 'optional' }],
aria: []
aria: [{ attr: 'aria-hidden', value: v.literal('true') }]
}
]
};

@ -1,4 +1,5 @@
import type { Morfo } from '../types';
import { v } from '../types';
export const comboboxMorfo: Morfo = {
name: 'Combobox',
@ -20,54 +21,162 @@ export const comboboxMorfo: Morfo = {
kebab: 'input',
kind: 'public',
defaultElement: 'input',
role: 'combobox',
optional: false,
states: ['open', 'closed'],
data: [
{ attr: 'data-state', values: ['open', 'closed'] },
{ attr: 'data-disabled', severity: 'optional' }
],
aria: []
aria: [
{ attr: 'aria-haspopup', value: v.literal('listbox') },
{ attr: 'aria-expanded', value: v.stateRef('open') },
{
attr: 'aria-controls',
value: v.partRef('content'),
condition: { when: 'part-present', part: 'content' },
severity: 'recommended'
},
{ attr: 'aria-activedescendant', value: v.propRef('highlightedId'), severity: 'optional' },
{ attr: 'aria-autocomplete', value: v.literal('list') },
{
attr: 'aria-required',
value: v.literal('true'),
severity: 'optional',
condition: { when: 'prop-truthy', prop: 'required' }
}
],
keyboard: [
{ key: 'ArrowDown', action: 'next-item' },
{ key: 'ArrowUp', action: 'prev-item' },
{ key: 'Home', action: 'first-item' },
{ key: 'End', action: 'last-item' },
{ key: 'Enter', action: 'select' },
{ key: 'Escape', action: 'close' }
]
},
{
name: 'Trigger',
kebab: 'trigger',
kind: 'public',
defaultElement: 'button',
role: 'button',
optional: true,
states: ['open', 'closed'],
data: [
{ attr: 'data-state', values: ['open', 'closed'] },
{ attr: 'data-disabled', severity: 'optional' }
],
aria: []
aria: [
{ attr: 'type', value: v.literal('button') },
{
attr: 'aria-label',
value: v.translationRef('#?components.combobox.toggle|Toggle'),
severity: 'recommended'
},
{ attr: 'aria-haspopup', value: v.literal('listbox') },
{ attr: 'aria-expanded', value: v.stateRef('open') },
{
attr: 'aria-controls',
value: v.partRef('content'),
condition: { when: 'part-present', part: 'content' },
severity: 'recommended'
}
]
},
{
name: 'Content',
kebab: 'content',
kind: 'public',
defaultElement: 'div',
role: 'listbox',
optional: false,
states: ['open', 'closed'],
data: [{ attr: 'data-state', values: ['open', 'closed'] }],
aria: [
{
attr: 'aria-labelledby',
value: v.partRef('input'),
condition: { when: 'part-present', part: 'input' },
severity: 'recommended'
},
{
attr: 'aria-multiselectable',
value: v.literal('true'),
severity: 'optional',
condition: { when: 'prop-truthy', prop: 'multiple' }
}
]
},
{
name: 'Viewport',
kebab: 'viewport',
kind: 'public',
defaultElement: 'div',
optional: true,
data: [],
aria: []
},
{ name: 'Viewport', kebab: 'viewport', kind: 'public', defaultElement: 'div', optional: true, data: [], aria: [] },
{
name: 'Item',
kebab: 'item',
kind: 'public',
defaultElement: 'div',
role: 'option',
optional: false,
states: ['checked', 'unchecked'],
data: [
{ attr: 'data-state', values: ['checked', 'unchecked'] },
{ attr: 'data-disabled', severity: 'optional' }
],
aria: [
{ attr: 'aria-selected', value: v.stateRef('checked') }
]
},
{
name: 'Group',
kebab: 'group',
kind: 'public',
defaultElement: 'div',
role: 'group',
optional: true,
data: [],
aria: [
{
attr: 'aria-labelledby',
value: v.partRef('group-heading'),
condition: { when: 'part-present', part: 'group-heading' },
severity: 'recommended'
}
]
},
{
name: 'GroupHeading',
kebab: 'group-heading',
kind: 'public',
defaultElement: 'div',
optional: true,
data: [],
aria: []
},
{ name: 'Group', kebab: 'group', kind: 'public', defaultElement: 'div', optional: true, data: [], aria: [] },
{ name: 'GroupHeading', kebab: 'group-heading', kind: 'public', defaultElement: 'div', optional: true, data: [], aria: [] },
{ name: 'Separator', kebab: 'separator', kind: 'public', defaultElement: 'div', optional: true, data: [], aria: [] },
{ name: 'Arrow', kebab: 'arrow', kind: 'public', defaultElement: 'div', optional: true, data: [], aria: [] }
{
name: 'Separator',
kebab: 'separator',
kind: 'public',
defaultElement: 'div',
role: 'separator',
optional: true,
data: [],
aria: []
},
{
name: 'Arrow',
kebab: 'arrow',
kind: 'public',
defaultElement: 'div',
optional: true,
data: [],
aria: []
}
]
};

@ -1,51 +1,189 @@
import type { Morfo } from '../types';
import { v } from '../types';
export const commandMorfo: Morfo = {
name: 'Command',
kebab: 'command',
scope: ['soma'],
parts: [
{ name: 'Provider', kebab: 'root', kind: 'public', defaultElement: 'div', optional: false, data: [], aria: [] },
{ name: 'Input', kebab: 'input', kind: 'public', defaultElement: 'input', optional: false, data: [], aria: [] },
{ name: 'List', kebab: 'list', kind: 'public', defaultElement: 'div', optional: false, data: [], aria: [] },
{ name: 'Viewport', kebab: 'viewport', kind: 'public', defaultElement: 'div', optional: true, data: [], aria: [] },
{
name: 'Provider',
kebab: 'root',
kind: 'public',
defaultElement: 'div',
optional: false,
data: [],
aria: []
},
{
name: 'Input',
kebab: 'input',
kind: 'public',
defaultElement: 'input',
role: 'combobox',
optional: false,
data: [],
aria: [
{ attr: 'aria-autocomplete', value: v.literal('list') },
{ attr: 'aria-expanded', value: v.literal('true') },
{
attr: 'aria-controls',
value: v.partRef('list'),
condition: { when: 'part-present', part: 'list' },
severity: 'recommended'
},
{ attr: 'aria-activedescendant', value: v.propRef('selectedId'), severity: 'optional' }
],
keyboard: [
{ key: 'ArrowDown', action: 'next-item' },
{ key: 'ArrowUp', action: 'prev-item' },
{ key: 'Home', action: 'first-item' },
{ key: 'End', action: 'last-item' },
{ key: 'Enter', action: 'select' }
]
},
{
name: 'List',
kebab: 'list',
kind: 'public',
defaultElement: 'div',
role: 'listbox',
optional: false,
data: [],
aria: [
{
attr: 'aria-label',
value: v.translationRef('#?components.command.list|Suggestions'),
severity: 'recommended'
}
]
},
{
name: 'Viewport',
kebab: 'viewport',
kind: 'public',
defaultElement: 'div',
optional: true,
data: [],
aria: []
},
{
name: 'Item',
kebab: 'item',
kind: 'public',
defaultElement: 'div',
role: 'option',
optional: false,
data: [
{ attr: 'data-selected', severity: 'optional' },
{ attr: 'data-disabled', severity: 'optional' }
],
aria: []
aria: [
{
attr: 'aria-selected',
value: v.propRef('selected'),
severity: 'optional'
},
{
attr: 'aria-disabled',
value: v.propRef('disabled'),
severity: 'optional'
}
]
},
{
name: 'LinkItem',
kebab: 'link-item',
kind: 'public',
defaultElement: 'a',
role: 'option',
optional: true,
data: [
{ attr: 'data-selected', severity: 'optional' },
{ attr: 'data-disabled', severity: 'optional' }
],
aria: []
aria: [
{
attr: 'aria-selected',
value: v.propRef('selected'),
severity: 'optional'
},
{
attr: 'aria-disabled',
value: v.propRef('disabled'),
severity: 'optional'
}
]
},
{
name: 'Group',
kebab: 'group',
kind: 'public',
defaultElement: 'div',
role: 'group',
optional: true,
data: [{ attr: 'data-empty', severity: 'optional' }],
aria: [
{
attr: 'aria-labelledby',
value: v.partRef('group-heading'),
condition: { when: 'part-present', part: 'group-heading' },
severity: 'recommended'
}
]
},
{
name: 'GroupHeading',
kebab: 'group-heading',
kind: 'public',
defaultElement: 'div',
optional: true,
data: [],
aria: [{ attr: 'aria-hidden', value: v.literal('true') }]
},
{
name: 'GroupItems',
kebab: 'group-items',
kind: 'public',
defaultElement: 'div',
role: 'presentation',
optional: true,
data: [],
aria: []
},
{ name: 'GroupHeading', kebab: 'group-heading', kind: 'public', defaultElement: 'div', optional: true, data: [], aria: [] },
{ name: 'GroupItems', kebab: 'group-items', kind: 'public', defaultElement: 'div', optional: true, data: [], aria: [] },
{ name: 'Empty', kebab: 'empty', kind: 'public', defaultElement: 'div', optional: true, data: [], aria: [] },
{ name: 'Loading', kebab: 'loading', kind: 'public', defaultElement: 'div', optional: true, data: [], aria: [] },
{ name: 'Separator', kebab: 'separator', kind: 'public', defaultElement: 'div', optional: true, data: [], aria: [] }
{
name: 'Empty',
kebab: 'empty',
kind: 'public',
defaultElement: 'div',
role: 'status',
optional: true,
data: [],
aria: [{ attr: 'aria-live', value: v.literal('polite') }]
},
{
name: 'Loading',
kebab: 'loading',
kind: 'public',
defaultElement: 'div',
role: 'progressbar',
optional: true,
data: [],
aria: [
{ attr: 'aria-valuemin', value: v.literal('0') },
{ attr: 'aria-valuemax', value: v.literal('100') },
{ attr: 'aria-valuenow', value: v.propRef('percent'), severity: 'optional' }
]
},
{
name: 'Separator',
kebab: 'separator',
kind: 'public',
defaultElement: 'div',
role: 'separator',
optional: true,
data: [],
aria: [{ attr: 'aria-orientation', value: v.literal('horizontal') }]
}
]
};

@ -1,12 +1,29 @@
import type { Morfo } from '../types';
import { v } from '../types';
export const contextMenuMorfo: Morfo = {
name: 'ContextMenu',
kebab: 'context-menu',
scope: ['soma'],
apg: 'https://www.w3.org/WAI/ARIA/apg/patterns/menu/',
focus: {
initial: 'first-focusable',
trap: true,
return: 'previous',
restore: true
},
parts: [
{ name: 'Provider', kebab: 'root', kind: 'virtual', defaultElement: 'none', optional: false, data: [], aria: [] },
{
name: 'Provider',
kebab: 'root',
kind: 'virtual',
defaultElement: 'none',
optional: false,
data: [],
aria: []
},
{
name: 'Trigger',
kebab: 'trigger',
@ -15,81 +32,217 @@ export const contextMenuMorfo: Morfo = {
optional: false,
states: ['open', 'closed'],
data: [{ attr: 'data-state', values: ['open', 'closed'] }],
aria: []
aria: [
{ attr: 'aria-haspopup', value: v.literal('menu') },
{ attr: 'aria-expanded', value: v.stateRef('open') },
{
attr: 'aria-controls',
value: v.partRef('content'),
condition: { when: 'part-present', part: 'content' },
severity: 'recommended'
}
]
},
{
name: 'Content',
kebab: 'content',
kind: 'public',
defaultElement: 'div',
role: 'menu',
optional: false,
states: ['open', 'closed'],
data: [{ attr: 'data-state', values: ['open', 'closed'] }],
aria: []
aria: [
{
attr: 'aria-labelledby',
value: v.partRef('trigger'),
condition: { when: 'part-present', part: 'trigger' },
severity: 'recommended'
},
{ attr: 'aria-orientation', value: v.literal('vertical') }
],
keyboard: [
{ key: 'Escape', action: 'close' },
{ key: 'ArrowDown', action: 'next-item' },
{ key: 'ArrowUp', action: 'prev-item' },
{ key: 'Home', action: 'first-item' },
{ key: 'End', action: 'last-item' },
{ key: 'Enter', action: 'activate' },
{ key: ' ', action: 'activate' }
]
},
{
name: 'Item',
kebab: 'item',
kind: 'public',
defaultElement: 'div',
role: 'menuitem',
optional: false,
data: [{ attr: 'data-disabled', severity: 'optional' }],
aria: []
aria: [
{
attr: 'aria-disabled',
value: v.literal('true'),
severity: 'optional',
condition: { when: 'prop-truthy', prop: 'disabled' }
}
]
},
{
name: 'CheckboxItem',
kebab: 'checkbox-item',
kind: 'public',
defaultElement: 'div',
role: 'menuitemcheckbox',
optional: true,
states: ['checked', 'unchecked'],
data: [
{ attr: 'data-state', values: ['checked', 'unchecked'] },
{ attr: 'data-disabled', severity: 'optional' }
],
aria: [
{ attr: 'aria-checked', value: v.stateRef('checked') },
{
attr: 'aria-disabled',
value: v.literal('true'),
severity: 'optional',
condition: { when: 'prop-truthy', prop: 'disabled' }
}
]
},
{
name: 'RadioGroup',
kebab: 'radio-group',
kind: 'public',
defaultElement: 'div',
role: 'group',
optional: true,
data: [],
aria: []
},
{ name: 'RadioGroup', kebab: 'radio-group', kind: 'public', defaultElement: 'div', optional: true, data: [], aria: [] },
{
name: 'RadioItem',
kebab: 'radio-item',
kind: 'public',
defaultElement: 'div',
role: 'menuitemradio',
optional: true,
states: ['checked', 'unchecked'],
data: [
{ attr: 'data-state', values: ['checked', 'unchecked'] },
{ attr: 'data-disabled', severity: 'optional' }
],
aria: [
{ attr: 'aria-checked', value: v.stateRef('checked') },
{
attr: 'aria-disabled',
value: v.literal('true'),
severity: 'optional',
condition: { when: 'prop-truthy', prop: 'disabled' }
}
]
},
{
name: 'Sub',
kebab: 'sub',
kind: 'virtual',
defaultElement: 'none',
optional: true,
data: [],
aria: []
},
{ name: 'Sub', kebab: 'sub', kind: 'virtual', defaultElement: 'none', optional: true, data: [], aria: [] },
{
name: 'SubTrigger',
kebab: 'sub-trigger',
kind: 'public',
defaultElement: 'div',
role: 'menuitem',
optional: true,
states: ['open', 'closed'],
data: [
{ attr: 'data-state', values: ['open', 'closed'] },
{ attr: 'data-disabled', severity: 'optional' }
],
aria: []
aria: [
{ attr: 'aria-haspopup', value: v.literal('menu') },
{ attr: 'aria-expanded', value: v.stateRef('open') },
{
attr: 'aria-controls',
value: v.partRef('sub-content'),
condition: { when: 'part-present', part: 'sub-content' },
severity: 'recommended'
},
{
attr: 'aria-disabled',
value: v.literal('true'),
severity: 'optional',
condition: { when: 'prop-truthy', prop: 'disabled' }
}
]
},
{
name: 'SubContent',
kebab: 'sub-content',
kind: 'public',
defaultElement: 'div',
role: 'menu',
optional: true,
states: ['open', 'closed'],
data: [{ attr: 'data-state', values: ['open', 'closed'] }],
aria: [
{
attr: 'aria-labelledby',
value: v.partRef('sub-trigger'),
condition: { when: 'part-present', part: 'sub-trigger' },
severity: 'recommended'
},
{ attr: 'aria-orientation', value: v.literal('vertical') }
]
},
{
name: 'Group',
kebab: 'group',
kind: 'public',
defaultElement: 'div',
role: 'group',
optional: true,
data: [],
aria: [
{
attr: 'aria-labelledby',
value: v.partRef('group-heading'),
condition: { when: 'part-present', part: 'group-heading' },
severity: 'recommended'
}
]
},
{
name: 'GroupHeading',
kebab: 'group-heading',
kind: 'public',
defaultElement: 'div',
optional: true,
data: [],
aria: []
},
{ name: 'Group', kebab: 'group', kind: 'public', defaultElement: 'div', optional: true, data: [], aria: [] },
{ name: 'GroupHeading', kebab: 'group-heading', kind: 'public', defaultElement: 'div', optional: true, data: [], aria: [] },
{ name: 'Separator', kebab: 'separator', kind: 'public', defaultElement: 'div', optional: true, data: [], aria: [] },
{ name: 'Arrow', kebab: 'arrow', kind: 'public', defaultElement: 'div', optional: true, data: [], aria: [] }
{
name: 'Separator',
kebab: 'separator',
kind: 'public',
defaultElement: 'div',
role: 'separator',
optional: true,
data: [],
aria: [{ attr: 'aria-orientation', value: v.literal('horizontal') }]
},
{
name: 'Arrow',
kebab: 'arrow',
kind: 'public',
defaultElement: 'div',
optional: true,
data: [],
aria: []
}
]
};

@ -1,4 +1,5 @@
import type { Morfo } from '../types';
import { v } from '../types';
export const dateFieldMorfo: Morfo = {
name: 'DateField',
@ -24,18 +25,29 @@ export const dateFieldMorfo: Morfo = {
kebab: 'input',
kind: 'public',
defaultElement: 'div',
role: 'group',
optional: false,
data: [
{ attr: 'data-invalid', severity: 'optional' },
{ attr: 'data-disabled', severity: 'optional' }
],
aria: []
aria: [
{ attr: 'aria-label', value: v.propRef('ariaLabel'), severity: 'optional' },
{
attr: 'aria-labelledby',
value: v.partRef('label'),
condition: { when: 'part-present', part: 'label' },
severity: 'recommended'
},
{ attr: 'aria-disabled', value: v.propRef('disabled'), severity: 'optional' }
]
},
{
name: 'Segment',
kebab: 'segment',
kind: 'public',
defaultElement: 'span',
role: 'spinbutton',
optional: false,
data: [
{ attr: 'data-invalid', severity: 'optional' },
@ -44,7 +56,23 @@ export const dateFieldMorfo: Morfo = {
{ attr: 'data-placeholder', severity: 'optional' },
{ attr: 'data-segment' }
],
aria: []
aria: [
{ attr: 'aria-label', value: v.propRef('segmentLabel') },
{ attr: 'aria-valuemin', value: v.propRef('min') },
{ attr: 'aria-valuemax', value: v.propRef('max') },
{ attr: 'aria-valuenow', value: v.propRef('value'), severity: 'optional' },
{ attr: 'aria-valuetext', value: v.propRef('valueText') },
{ attr: 'aria-invalid', value: v.propRef('invalid'), severity: 'optional' },
{ attr: 'aria-disabled', value: v.propRef('disabled'), severity: 'optional' },
{ attr: 'aria-readonly', value: v.propRef('readonly'), severity: 'optional' }
],
keyboard: [
{ key: 'ArrowUp', action: 'increment' },
{ key: 'ArrowDown', action: 'decrement' },
{ key: 'Backspace', action: 'delete' },
{ key: 'ArrowLeft', action: 'prev-segment' },
{ key: 'ArrowRight', action: 'next-segment' }
]
},
{
name: 'Label',

@ -1,21 +1,63 @@
import type { Morfo } from '../types';
import { v } from '../types';
export const datePickerMorfo: Morfo = {
name: 'DatePicker',
kebab: 'date-picker',
scope: ['soma'],
focus: {
initial: 'first-focusable',
trap: true,
return: 'trigger',
restore: true
},
parts: [
{ name: 'Provider', kebab: 'root', kind: 'virtual', defaultElement: 'none', optional: false, data: [], aria: [] },
{
name: 'Provider',
kebab: 'root',
kind: 'virtual',
defaultElement: 'none',
optional: false,
data: [],
aria: []
},
{
name: 'Trigger',
kebab: 'trigger',
kind: 'public',
defaultElement: 'button',
role: 'button',
optional: false,
states: ['open', 'closed'],
data: [{ attr: 'data-state', values: ['open', 'closed'] }],
aria: []
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('calendar'),
condition: { when: 'part-present', part: 'calendar' },
severity: 'recommended'
},
{
attr: 'aria-label',
value: v.translationRef('#?components.date-picker.trigger|Choose date'),
severity: 'recommended'
}
]
},
{ name: 'Calendar', kebab: 'calendar', kind: 'public', defaultElement: 'div', optional: false, data: [], aria: [] }
{
name: 'Calendar',
kebab: 'calendar',
kind: 'public',
defaultElement: 'div',
role: 'dialog',
optional: false,
data: [],
aria: []
}
]
};

@ -1,4 +1,5 @@
import type { Morfo } from '../types';
import { v } from '../types';
export const dateRangeFieldMorfo: Morfo = {
name: 'DateRangeField',
@ -10,6 +11,7 @@ export const dateRangeFieldMorfo: Morfo = {
kebab: 'root',
kind: 'public',
defaultElement: 'div',
role: 'group',
optional: false,
data: [
{ attr: 'data-invalid', severity: 'optional' },
@ -17,7 +19,16 @@ export const dateRangeFieldMorfo: Morfo = {
{ attr: 'data-readonly', severity: 'optional' },
{ attr: 'data-required', severity: 'optional' }
],
aria: []
aria: [
{ attr: 'aria-label', value: v.propRef('ariaLabel'), severity: 'optional' },
{
attr: 'aria-labelledby',
value: v.partRef('label'),
condition: { when: 'part-present', part: 'label' },
severity: 'recommended'
},
{ attr: 'aria-disabled', value: v.propRef('disabled'), severity: 'optional' }
]
},
{
name: 'Label',

@ -1,21 +1,63 @@
import type { Morfo } from '../types';
import { v } from '../types';
export const dateRangePickerMorfo: Morfo = {
name: 'DateRangePicker',
kebab: 'date-range-picker',
scope: ['soma'],
focus: {
initial: 'first-focusable',
trap: true,
return: 'trigger',
restore: true
},
parts: [
{ name: 'Provider', kebab: 'root', kind: 'virtual', defaultElement: 'none', optional: false, data: [], aria: [] },
{
name: 'Provider',
kebab: 'root',
kind: 'virtual',
defaultElement: 'none',
optional: false,
data: [],
aria: []
},
{
name: 'Trigger',
kebab: 'trigger',
kind: 'public',
defaultElement: 'button',
role: 'button',
optional: false,
states: ['open', 'closed'],
data: [{ attr: 'data-state', values: ['open', 'closed'] }],
aria: []
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('calendar'),
condition: { when: 'part-present', part: 'calendar' },
severity: 'recommended'
},
{
attr: 'aria-label',
value: v.translationRef('#?components.date-range-picker.trigger|Choose date range'),
severity: 'recommended'
}
]
},
{ name: 'Calendar', kebab: 'calendar', kind: 'public', defaultElement: 'div', optional: false, data: [], aria: [] }
{
name: 'Calendar',
kebab: 'calendar',
kind: 'public',
defaultElement: 'div',
role: 'dialog',
optional: false,
data: [],
aria: []
}
]
};

@ -1,4 +1,5 @@
import type { Morfo } from '../types';
import { v } from '../types';
export const dragDropMorfo: Morfo = {
name: 'DragDrop',
@ -19,25 +20,67 @@ export const dragDropMorfo: Morfo = {
kebab: 'draggable',
kind: 'public',
defaultElement: 'div',
role: 'button',
optional: false,
data: [
{ attr: 'data-dragging', severity: 'optional' },
{ attr: 'data-disabled', severity: 'optional' }
],
aria: []
aria: [
{
attr: 'aria-roledescription',
value: v.translationRef('#?components.drag-drop.draggable|Draggable')
},
{
attr: 'aria-disabled',
value: v.literal('true'),
severity: 'optional',
condition: { when: 'prop-truthy', prop: 'disabled' }
},
{
attr: 'aria-grabbed',
value: v.literal('true'),
severity: 'optional',
condition: { when: 'prop-truthy', prop: 'active' }
}
],
keyboard: [
{ key: 'Enter', action: 'activate' },
{ key: ' ', action: 'activate' },
{ key: 'Escape', action: 'cancel' },
{ key: 'ArrowDown', action: 'next' },
{ key: 'ArrowRight', action: 'next' },
{ key: 'ArrowUp', action: 'prev' },
{ key: 'ArrowLeft', action: 'prev' },
{ key: 'Tab', action: 'next' },
{ key: 'Shift+Tab', action: 'prev' }
]
},
{
name: 'Droppable',
kebab: 'droppable',
kind: 'public',
defaultElement: 'div',
role: 'region',
optional: false,
data: [
{ attr: 'data-dragover', severity: 'optional' },
{ attr: 'data-accepting', severity: 'optional' },
{ attr: 'data-disabled', severity: 'optional' }
],
aria: []
aria: [
{
attr: 'aria-roledescription',
value: v.translationRef('#?components.drag-drop.droppable|Drop zone')
},
{ attr: 'aria-dropeffect', value: v.propRef('dropEffect'), severity: 'optional' },
{
attr: 'aria-disabled',
value: v.literal('true'),
severity: 'optional',
condition: { when: 'prop-truthy', prop: 'disabled' }
}
]
},
{
name: 'Preview',
@ -46,7 +89,7 @@ export const dragDropMorfo: Morfo = {
defaultElement: 'div',
optional: true,
data: [{ attr: 'data-active', severity: 'optional' }],
aria: []
aria: [{ attr: 'aria-hidden', value: v.literal('true') }]
}
]
};

@ -1,10 +1,19 @@
import type { Morfo } from '../types';
import { v } from '../types';
export const drawerMorfo: Morfo = {
name: 'Drawer',
kebab: 'drawer',
scope: ['soma'],
apg: 'https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/',
focus: {
initial: 'first-focusable',
trap: true,
return: 'trigger',
restore: true
},
parts: [
{
name: 'Provider',
@ -21,16 +30,33 @@ export const drawerMorfo: Morfo = {
kebab: 'trigger',
kind: 'public',
defaultElement: 'button',
role: 'button',
optional: false,
states: ['open', 'closed'],
data: [{ attr: 'data-state', values: ['open', 'closed'] }],
aria: []
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'),
condition: { when: 'part-present', part: 'content' },
severity: 'recommended'
},
{
attr: 'aria-label',
value: v.translationRef('#?components.drawer.trigger|Open drawer'),
severity: 'recommended'
}
]
},
{
name: 'Content',
kebab: 'content',
kind: 'public',
defaultElement: 'div',
role: 'dialog',
optional: false,
states: ['open', 'closed'],
data: [
@ -38,7 +64,31 @@ export const drawerMorfo: Morfo = {
{ attr: 'data-side', values: ['top', 'bottom', 'left', 'right'] },
{ attr: 'data-dragging', severity: 'optional' }
],
aria: []
aria: [
{
attr: 'aria-modal',
value: v.literal('true'),
severity: 'optional',
condition: { when: 'prop-truthy', prop: 'modal' }
},
{
attr: 'aria-labelledby',
value: v.partRef('title'),
condition: { when: 'part-present', part: 'title' },
severity: 'recommended'
},
{
attr: 'aria-describedby',
value: v.partRef('description'),
condition: { when: 'part-present', part: 'description' },
severity: 'optional'
}
],
keyboard: [
{ key: 'Escape', action: 'close' },
{ key: 'Tab', action: 'focus-next' },
{ key: 'Shift+Tab', action: 'focus-prev' }
]
},
{
name: 'Overlay',
@ -48,11 +98,52 @@ export const drawerMorfo: Morfo = {
optional: true,
states: ['open', 'closed'],
data: [{ attr: 'data-state', values: ['open', 'closed'] }],
aria: [{ attr: 'aria-hidden', value: v.literal('true') }]
},
{
name: 'Handle',
kebab: 'handle',
kind: 'public',
defaultElement: 'div',
optional: true,
data: [],
aria: [{ attr: 'aria-hidden', value: v.literal('true') }]
},
{
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: 'Handle', kebab: 'handle', kind: 'public', defaultElement: 'div', optional: true, data: [], aria: [] },
{ name: 'Title', kebab: 'title', kind: 'public', defaultElement: 'div', optional: true, data: [], aria: [] },
{ name: 'Description', kebab: 'description', kind: 'public', defaultElement: 'div', optional: true, data: [], aria: [] },
{ name: 'Close', kebab: 'close', kind: 'public', defaultElement: 'button', 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.translationRef('#?components.drawer.close|Close'),
severity: 'recommended'
}
]
}
]
};

@ -1,95 +1,255 @@
import type { Morfo } from '../types';
import { v } from '../types';
export const dropdownMenuMorfo: Morfo = {
name: 'DropdownMenu',
kebab: 'dropdown-menu',
scope: ['soma'],
apg: 'https://www.w3.org/WAI/ARIA/apg/patterns/menu-button/',
focus: {
initial: 'first-focusable',
trap: true,
return: 'trigger',
restore: true
},
parts: [
{ name: 'Provider', kebab: 'root', kind: 'virtual', defaultElement: 'none', optional: false, data: [], aria: [] },
{
name: 'Provider',
kebab: 'root',
kind: 'virtual',
defaultElement: 'none',
optional: false,
data: [],
aria: []
},
{
name: 'Trigger',
kebab: 'trigger',
kind: 'public',
defaultElement: 'button',
role: 'button',
optional: false,
states: ['open', 'closed'],
data: [{ attr: 'data-state', values: ['open', 'closed'] }],
aria: []
aria: [
{ attr: 'type', value: v.literal('button') },
{ attr: 'aria-haspopup', value: v.literal('menu') },
{ attr: 'aria-expanded', value: v.stateRef('open') },
{
attr: 'aria-controls',
value: v.partRef('content'),
condition: { when: 'part-present', part: 'content' },
severity: 'recommended'
}
],
keyboard: [
{ key: 'Enter', action: 'open' },
{ key: ' ', action: 'open' },
{ key: 'ArrowDown', action: 'open-first' }
]
},
{
name: 'Content',
kebab: 'content',
kind: 'public',
defaultElement: 'div',
role: 'menu',
optional: false,
states: ['open', 'closed'],
data: [{ attr: 'data-state', values: ['open', 'closed'] }],
aria: []
aria: [
{
attr: 'aria-labelledby',
value: v.partRef('trigger'),
condition: { when: 'part-present', part: 'trigger' },
severity: 'recommended'
},
{ attr: 'aria-orientation', value: v.literal('vertical') }
],
keyboard: [
{ key: 'Escape', action: 'close' },
{ key: 'ArrowDown', action: 'next-item' },
{ key: 'ArrowUp', action: 'prev-item' },
{ key: 'Home', action: 'first-item' },
{ key: 'End', action: 'last-item' },
{ key: 'Enter', action: 'activate' },
{ key: ' ', action: 'activate' }
]
},
{
name: 'Item',
kebab: 'item',
kind: 'public',
defaultElement: 'div',
role: 'menuitem',
optional: false,
data: [{ attr: 'data-disabled', severity: 'optional' }],
aria: []
aria: [
{
attr: 'aria-disabled',
value: v.literal('true'),
severity: 'optional',
condition: { when: 'prop-truthy', prop: 'disabled' }
}
]
},
{
name: 'CheckboxItem',
kebab: 'checkbox-item',
kind: 'public',
defaultElement: 'div',
role: 'menuitemcheckbox',
optional: true,
states: ['checked', 'unchecked'],
data: [
{ attr: 'data-state', values: ['checked', 'unchecked'] },
{ attr: 'data-disabled', severity: 'optional' }
],
aria: [
{ attr: 'aria-checked', value: v.stateRef('checked') },
{
attr: 'aria-disabled',
value: v.literal('true'),
severity: 'optional',
condition: { when: 'prop-truthy', prop: 'disabled' }
}
]
},
{
name: 'RadioGroup',
kebab: 'radio-group',
kind: 'public',
defaultElement: 'div',
role: 'group',
optional: true,
data: [],
aria: []
},
{ name: 'RadioGroup', kebab: 'radio-group', kind: 'public', defaultElement: 'div', optional: true, data: [], aria: [] },
{
name: 'RadioItem',
kebab: 'radio-item',
kind: 'public',
defaultElement: 'div',
role: 'menuitemradio',
optional: true,
states: ['checked', 'unchecked'],
data: [
{ attr: 'data-state', values: ['checked', 'unchecked'] },
{ attr: 'data-disabled', severity: 'optional' }
],
aria: [
{ attr: 'aria-checked', value: v.stateRef('checked') },
{
attr: 'aria-disabled',
value: v.literal('true'),
severity: 'optional',
condition: { when: 'prop-truthy', prop: 'disabled' }
}
]
},
{
name: 'Sub',
kebab: 'sub',
kind: 'virtual',
defaultElement: 'none',
optional: true,
data: [],
aria: []
},
{ name: 'Sub', kebab: 'sub', kind: 'virtual', defaultElement: 'none', optional: true, data: [], aria: [] },
{
name: 'SubTrigger',
kebab: 'sub-trigger',
kind: 'public',
defaultElement: 'div',
role: 'menuitem',
optional: true,
states: ['open', 'closed'],
data: [
{ attr: 'data-state', values: ['open', 'closed'] },
{ attr: 'data-disabled', severity: 'optional' }
],
aria: []
aria: [
{ attr: 'aria-haspopup', value: v.literal('menu') },
{ attr: 'aria-expanded', value: v.stateRef('open') },
{
attr: 'aria-controls',
value: v.partRef('sub-content'),
condition: { when: 'part-present', part: 'sub-content' },
severity: 'recommended'
},
{
attr: 'aria-disabled',
value: v.literal('true'),
severity: 'optional',
condition: { when: 'prop-truthy', prop: 'disabled' }
}
]
},
{
name: 'SubContent',
kebab: 'sub-content',
kind: 'public',
defaultElement: 'div',
role: 'menu',
optional: true,
states: ['open', 'closed'],
data: [{ attr: 'data-state', values: ['open', 'closed'] }],
aria: [
{
attr: 'aria-labelledby',
value: v.partRef('sub-trigger'),
condition: { when: 'part-present', part: 'sub-trigger' },
severity: 'recommended'
},
{ attr: 'aria-orientation', value: v.literal('vertical') }
]
},
{
name: 'Group',
kebab: 'group',
kind: 'public',
defaultElement: 'div',
role: 'group',
optional: true,
data: [],
aria: [
{
attr: 'aria-labelledby',
value: v.partRef('group-heading'),
condition: { when: 'part-present', part: 'group-heading' },
severity: 'recommended'
}
]
},
{
name: 'GroupHeading',
kebab: 'group-heading',
kind: 'public',
defaultElement: 'div',
optional: true,
data: [],
aria: []
},
{ name: 'Group', kebab: 'group', kind: 'public', defaultElement: 'div', optional: true, data: [], aria: [] },
{ name: 'GroupHeading', kebab: 'group-heading', kind: 'public', defaultElement: 'div', optional: true, data: [], aria: [] },
{ name: 'Separator', kebab: 'separator', kind: 'public', defaultElement: 'div', optional: true, data: [], aria: [] },
{ name: 'Arrow', kebab: 'arrow', kind: 'public', defaultElement: 'div', optional: true, data: [], aria: [] }
{
name: 'Separator',
kebab: 'separator',
kind: 'public',
defaultElement: 'div',
role: 'separator',
optional: true,
data: [],
aria: [{ attr: 'aria-orientation', value: v.literal('horizontal') }]
},
{
name: 'Arrow',
kebab: 'arrow',
kind: 'public',
defaultElement: 'div',
optional: true,
data: [],
aria: []
}
]
};

@ -1,4 +1,5 @@
import type { Morfo } from '../types';
import { v } from '../types';
export const editableMorfo: Morfo = {
name: 'Editable',
@ -19,19 +20,31 @@ export const editableMorfo: Morfo = {
],
aria: []
},
{ name: 'Area', kebab: 'area', kind: 'public', defaultElement: 'div', optional: true, data: [], aria: [] },
{
name: 'Area',
kebab: 'area',
kind: 'public',
defaultElement: 'div',
optional: true,
data: [],
aria: []
},
{
name: 'Preview',
kebab: 'preview',
kind: 'public',
defaultElement: 'div',
role: 'textbox',
optional: false,
data: [
{ attr: 'data-placeholder-shown', severity: 'optional' },
{ attr: 'data-disabled', severity: 'optional' },
{ attr: 'data-readonly', severity: 'optional' }
],
aria: []
aria: [
{ attr: 'aria-readonly', value: v.literal('true') },
{ attr: 'aria-label', value: v.propRef('placeholder'), severity: 'optional' }
]
},
{
name: 'Input',
@ -43,26 +56,69 @@ export const editableMorfo: Morfo = {
{ attr: 'data-disabled', severity: 'optional' },
{ attr: 'data-readonly', severity: 'optional' }
],
aria: []
aria: [
{
attr: 'aria-disabled',
value: v.literal('true'),
severity: 'optional',
condition: { when: 'prop-truthy', prop: 'disabled' }
}
],
keyboard: [
{ key: 'Enter', action: 'submit' },
{ key: 'Escape', action: 'cancel' }
]
},
{
name: 'EditTrigger',
kebab: 'edit-trigger',
kind: 'public',
defaultElement: 'button',
role: 'button',
optional: true,
data: [],
aria: [
{ attr: 'type', value: v.literal('button') },
{
attr: 'aria-label',
value: v.translationRef('#?components.editable.edit|Edit'),
severity: 'recommended'
}
]
},
{ name: 'EditTrigger', kebab: 'edit-trigger', kind: 'public', defaultElement: 'button', optional: true, data: [], aria: [] },
{
name: 'SubmitTrigger',
kebab: 'submit-trigger',
kind: 'public',
defaultElement: 'button',
role: 'button',
optional: true,
data: [{ attr: 'data-disabled', severity: 'optional' }],
aria: []
aria: [
{ attr: 'type', value: v.literal('button') },
{
attr: 'aria-label',
value: v.translationRef('#?components.editable.submit|Submit'),
severity: 'recommended'
}
]
},
{
name: 'CancelTrigger',
kebab: 'cancel-trigger',
kind: 'public',
defaultElement: 'button',
role: 'button',
optional: true,
data: [{ attr: 'data-disabled', severity: 'optional' }],
aria: []
aria: [
{ attr: 'type', value: v.literal('button') },
{
attr: 'aria-label',
value: v.translationRef('#?components.editable.cancel|Cancel'),
severity: 'recommended'
}
]
}
]
};

@ -1,4 +1,5 @@
import type { Morfo } from '../types';
import { v } from '../types';
export const feedMorfo: Morfo = {
name: 'Feed',
@ -11,32 +12,92 @@ export const feedMorfo: Morfo = {
kebab: 'root',
kind: 'public',
defaultElement: 'section',
role: 'feed',
optional: false,
data: [{ attr: 'data-busy', severity: 'optional' }],
aria: []
aria: [
{
attr: 'aria-label',
value: v.translationRef('#?components.feed.label|Feed'),
severity: 'recommended'
},
{
attr: 'aria-busy',
value: v.literal('true'),
severity: 'optional',
condition: { when: 'prop-truthy', prop: 'busy' }
}
],
keyboard: [
{ key: 'PageDown', action: 'next-item' },
{ key: 'PageUp', action: 'prev-item' },
{ key: 'Ctrl+Home', action: 'first-item' },
{ key: 'Ctrl+End', action: 'last-item' }
]
},
{
name: 'Article',
kebab: 'article',
kind: 'public',
defaultElement: 'article',
role: 'article',
optional: false,
data: [
{ attr: 'data-posinset' },
{ attr: 'data-level' }
],
aria: [
{ attr: 'aria-posinset', value: v.propRef('posinset') },
{ attr: 'aria-setsize', value: v.propRef('setsize') },
{
attr: 'aria-labelledby',
value: v.partRef('article-title'),
condition: { when: 'part-present', part: 'article-title' },
severity: 'recommended'
},
{
attr: 'aria-describedby',
value: v.partRef('article-description'),
condition: { when: 'part-present', part: 'article-description' },
severity: 'optional'
}
]
},
{
name: 'ArticleTitle',
kebab: 'article-title',
kind: 'public',
defaultElement: 'div',
role: 'heading',
optional: true,
data: [],
aria: [{ attr: 'aria-level', value: v.propRef('level') }]
},
{
name: 'ArticleDescription',
kebab: 'article-description',
kind: 'public',
defaultElement: 'div',
optional: true,
data: [],
aria: []
},
{ name: 'ArticleTitle', kebab: 'article-title', kind: 'public', defaultElement: 'div', optional: true, data: [], aria: [] },
{ name: 'ArticleDescription', kebab: 'article-description', kind: 'public', defaultElement: 'div', optional: true, data: [], aria: [] },
{
name: 'Thread',
kebab: 'thread',
kind: 'public',
defaultElement: 'div',
role: 'feed',
optional: true,
data: [{ attr: 'data-level' }],
aria: []
aria: [
{
attr: 'aria-labelledby',
value: v.partRef('article-title'),
condition: { when: 'part-present', part: 'article-title' },
severity: 'optional'
}
]
},
{
name: 'Sentinel',
@ -45,7 +106,7 @@ export const feedMorfo: Morfo = {
defaultElement: 'div',
optional: true,
data: [{ attr: 'data-intersecting', severity: 'optional' }],
aria: []
aria: [{ attr: 'aria-hidden', value: v.literal('true') }]
}
]
};

@ -1,4 +1,5 @@
import type { Morfo } from '../types';
import { v } from '../types';
export const fieldMorfo: Morfo = {
name: 'Field',
@ -35,7 +36,14 @@ export const fieldMorfo: Morfo = {
{ attr: 'data-invalid', severity: 'optional' },
{ attr: 'data-required', severity: 'optional' }
],
aria: []
aria: [
{
attr: 'for',
value: v.partRef('input'),
condition: { when: 'part-present', part: 'input' },
severity: 'recommended'
}
]
},
{
name: 'Control',
@ -59,7 +67,34 @@ export const fieldMorfo: Morfo = {
optional: true,
states: ['idle', 'error', 'disabled', 'readonly', 'loading'],
data: [{ attr: 'data-state', values: ['idle', 'error', 'disabled', 'readonly', 'loading'] }],
aria: []
aria: [
{ attr: 'type', value: v.propRef('type') },
{
attr: 'aria-labelledby',
value: v.partRef('label'),
condition: { when: 'part-present', part: 'label' },
severity: 'recommended'
},
{ attr: 'aria-invalid', value: v.propRef('invalid') },
{
attr: 'aria-describedby',
value: v.partRef('helper-text'),
condition: { when: 'part-present', part: 'helper-text' },
severity: 'recommended'
},
{
attr: 'aria-busy',
value: v.literal('true'),
severity: 'optional',
condition: { when: 'state-equals', state: 'loading', value: 'true' }
},
{
attr: 'aria-required',
value: v.literal('true'),
severity: 'optional',
condition: { when: 'prop-truthy', prop: 'required' }
}
]
},
{
name: 'HelperText',
@ -70,8 +105,33 @@ export const fieldMorfo: Morfo = {
data: [{ attr: 'data-disabled', severity: 'optional' }],
aria: []
},
{ name: 'ErrorText', kebab: 'error-text', kind: 'public', defaultElement: 'div', optional: true, data: [], aria: [] },
{ name: 'Prefix', kebab: 'prefix', kind: 'public', defaultElement: 'div', optional: true, data: [], aria: [] },
{ name: 'Suffix', kebab: 'suffix', kind: 'public', defaultElement: 'div', optional: true, data: [], aria: [] }
{
name: 'ErrorText',
kebab: 'error-text',
kind: 'public',
defaultElement: 'div',
role: 'alert',
optional: true,
data: [],
aria: [{ attr: 'aria-live', value: v.literal('polite') }]
},
{
name: 'Prefix',
kebab: 'prefix',
kind: 'public',
defaultElement: 'div',
optional: true,
data: [],
aria: [{ attr: 'aria-hidden', value: v.literal('true') }]
},
{
name: 'Suffix',
kebab: 'suffix',
kind: 'public',
defaultElement: 'div',
optional: true,
data: [],
aria: [{ attr: 'aria-hidden', value: v.literal('true') }]
}
]
};

@ -1,4 +1,5 @@
import type { Morfo } from '../types';
import { v } from '../types';
export const fileUploadMorfo: Morfo = {
name: 'FileUpload',
@ -33,27 +34,96 @@ export const fileUploadMorfo: Morfo = {
kebab: 'dropzone',
kind: 'public',
defaultElement: 'div',
role: 'button',
optional: true,
data: [
{ attr: 'data-disabled', severity: 'optional' },
{ attr: 'data-dragging', severity: 'optional' }
],
aria: []
aria: [
{
attr: 'aria-label',
value: v.translationRef('#?components.file-upload.dropzone|Upload files'),
severity: 'recommended'
},
{ attr: 'aria-disabled', value: v.propRef('disabled'), severity: 'optional' }
],
keyboard: [
{ key: 'Enter', action: 'open-file-picker' },
{ key: ' ', action: 'open-file-picker' }
]
},
{
name: 'Trigger',
kebab: 'trigger',
kind: 'public',
defaultElement: 'button',
role: 'button',
optional: true,
data: [{ attr: 'data-disabled', severity: 'optional' }],
aria: [
{ attr: 'type', value: v.literal('button') },
{
attr: 'aria-label',
value: v.translationRef('#?components.file-upload.trigger|Select files'),
severity: 'recommended'
},
{ attr: 'aria-disabled', value: v.propRef('disabled'), severity: 'optional' }
]
},
{
name: 'HiddenInput',
kebab: 'hidden-input',
kind: 'public',
defaultElement: 'input',
optional: true,
data: [],
aria: [{ attr: 'aria-hidden', value: v.literal('true') }]
},
{
name: 'FileList',
kebab: 'file-list',
kind: 'public',
defaultElement: 'ul',
role: 'list',
optional: true,
data: [],
aria: [
{
attr: 'aria-label',
value: v.translationRef('#?components.file-upload.list|Selected files'),
severity: 'recommended'
}
]
},
{
name: 'Item',
kebab: 'item',
kind: 'public',
defaultElement: 'li',
role: 'listitem',
optional: true,
data: [],
aria: []
},
{
name: 'ItemName',
kebab: 'item-name',
kind: 'public',
defaultElement: 'span',
optional: true,
data: [],
aria: []
},
{
name: 'ItemSize',
kebab: 'item-size',
kind: 'public',
defaultElement: 'span',
optional: true,
data: [],
aria: []
},
{ name: 'HiddenInput', kebab: 'hidden-input', kind: 'public', defaultElement: 'input', optional: true, data: [], aria: [] },
{ name: 'FileList', kebab: 'file-list', kind: 'public', defaultElement: 'ul', optional: true, data: [], aria: [] },
{ name: 'Item', kebab: 'item', kind: 'public', defaultElement: 'li', optional: true, data: [], aria: [] },
{ name: 'ItemName', kebab: 'item-name', kind: 'public', defaultElement: 'span', optional: true, data: [], aria: [] },
{ name: 'ItemSize', kebab: 'item-size', kind: 'public', defaultElement: 'span', optional: true, data: [], aria: [] },
{
name: 'ItemPreview',
kebab: 'item-preview',
@ -61,26 +131,56 @@ export const fileUploadMorfo: Morfo = {
defaultElement: 'div',
optional: true,
data: [{ attr: 'data-type' }],
aria: []
aria: [{ attr: 'aria-label', value: v.propRef('fileName'), severity: 'recommended' }]
},
{
name: 'ItemRemove',
kebab: 'item-remove',
kind: 'public',
defaultElement: 'button',
role: 'button',
optional: true,
data: [{ attr: 'data-disabled', severity: 'optional' }],
aria: []
aria: [
{ attr: 'type', value: v.literal('button') },
{
attr: 'aria-label',
value: v.translationRef('#?components.file-upload.remove|Remove file'),
severity: 'recommended'
}
]
},
{
name: 'ItemProgress',
kebab: 'item-progress',
kind: 'public',
defaultElement: 'div',
role: 'progressbar',
optional: true,
data: [],
aria: [
{ attr: 'aria-valuenow', value: v.propRef('percent') },
{ attr: 'aria-valuemin', value: v.literal('0') },
{ attr: 'aria-valuemax', value: v.literal('100') },
{ attr: 'aria-label', value: v.propRef('fileName'), severity: 'recommended' }
]
},
{ name: 'ItemProgress', kebab: 'item-progress', kind: 'public', defaultElement: 'div', optional: true, data: [], aria: [] },
{
name: 'ClearTrigger',
kebab: 'clear-trigger',
kind: 'public',
defaultElement: 'button',
role: 'button',
optional: true,
data: [{ attr: 'data-disabled', severity: 'optional' }],
aria: []
aria: [
{ attr: 'type', value: v.literal('button') },
{
attr: 'aria-label',
value: v.translationRef('#?components.file-upload.clear|Clear all'),
severity: 'recommended'
}
]
}
]
};

@ -1,4 +1,5 @@
import type { Morfo } from '../types';
import { v } from '../types';
export const formMorfo: Morfo = {
name: 'Form',
@ -18,34 +19,64 @@ export const formMorfo: Morfo = {
{ attr: 'data-invalid', severity: 'optional' },
{ attr: 'data-submitted', severity: 'optional' }
],
aria: []
aria: [
{
attr: 'aria-busy',
value: v.literal('true'),
severity: 'optional',
condition: { when: 'prop-truthy', prop: 'isPending' }
}
]
},
{
name: 'Submit',
kebab: 'submit',
kind: 'public',
defaultElement: 'button',
role: 'button',
optional: true,
data: [{ attr: 'data-pending', severity: 'optional' }],
aria: []
aria: [
{ attr: 'type', value: v.literal('submit') },
{
attr: 'aria-label',
value: v.translationRef('#?components.form.submit|Submit'),
severity: 'recommended'
},
{
attr: 'aria-busy',
value: v.literal('true'),
severity: 'optional',
condition: { when: 'prop-truthy', prop: 'isPending' }
}
]
},
{
name: 'Reset',
kebab: 'reset',
kind: 'public',
defaultElement: 'button',
role: 'button',
optional: true,
data: [{ attr: 'data-dirty', severity: 'optional' }],
aria: []
aria: [
{ attr: 'type', value: v.literal('button') },
{
attr: 'aria-label',
value: v.translationRef('#?components.form.reset|Reset'),
severity: 'recommended'
}
]
},
{
name: 'ErrorSummary',
kebab: 'error-summary',
kind: 'public',
defaultElement: 'div',
role: 'alert',
optional: true,
data: [{ attr: 'data-hidden', severity: 'optional' }],
aria: []
aria: [{ attr: 'aria-live', value: v.literal('polite') }]
}
]
};

@ -1,4 +1,5 @@
import type { Morfo } from '../types';
import { v } from '../types';
export const gridListMorfo: Morfo = {
name: 'GridList',
@ -11,6 +12,7 @@ export const gridListMorfo: Morfo = {
kebab: 'root',
kind: 'public',
defaultElement: 'div',
role: 'grid',
optional: false,
data: [
{ attr: 'data-disabled', severity: 'optional' },
@ -20,13 +22,58 @@ export const gridListMorfo: Morfo = {
{ attr: 'data-empty', severity: 'optional' },
{ attr: 'data-selection-mode', values: ['none', 'single', 'multiple'] }
],
aria: []
aria: [
{
attr: 'aria-label',
value: v.translationRef('#?components.grid-list.label|List'),
severity: 'recommended'
},
{
attr: 'aria-multiselectable',
value: v.literal('true'),
severity: 'optional',
condition: { when: 'prop-truthy', prop: 'multiple' }
},
{
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' }
},
{
attr: 'aria-required',
value: v.literal('true'),
severity: 'optional',
condition: { when: 'prop-truthy', prop: 'required' }
}
],
keyboard: [
{ key: 'Home', action: 'first-item' },
{ key: 'End', action: 'last-item' },
{ key: 'PageUp', action: 'page-up' },
{ key: 'PageDown', action: 'page-down' },
{ key: ' ', action: 'select' },
{ key: 'Escape', action: 'cancel' }
]
},
{
name: 'Row',
kebab: 'row',
kind: 'public',
defaultElement: 'div',
role: 'row',
optional: false,
states: ['selected', 'unselected'],
data: [
@ -34,21 +81,58 @@ export const gridListMorfo: Morfo = {
{ attr: 'data-highlighted', severity: 'optional' },
{ attr: 'data-disabled', severity: 'optional' }
],
aria: [
{ attr: 'aria-selected', value: v.stateRef('selected'), severity: 'optional' },
{
attr: 'aria-disabled',
value: v.literal('true'),
severity: 'optional',
condition: { when: 'prop-truthy', prop: 'disabled' }
}
],
keyboard: [
{ key: 'ArrowDown', action: 'next-row' },
{ key: 'ArrowUp', action: 'prev-row' },
{ key: 'ArrowRight', action: 'next-cell' },
{ key: 'ArrowLeft', action: 'prev-cell' }
]
},
{
name: 'Cell',
kebab: 'cell',
kind: 'public',
defaultElement: 'div',
role: 'gridcell',
optional: false,
data: [],
aria: []
},
{ name: 'Cell', kebab: 'cell', kind: 'public', defaultElement: 'div', optional: false, data: [], aria: [] },
{
name: 'SelectionCheckbox',
kebab: 'selection-checkbox',
kind: 'public',
defaultElement: 'button',
role: 'checkbox',
optional: true,
states: ['checked', 'unchecked'],
data: [
{ attr: 'data-state', values: ['checked', 'unchecked'] },
{ attr: 'data-disabled', severity: 'optional' }
],
aria: []
aria: [
{ attr: 'aria-checked', value: v.stateRef('checked') },
{
attr: 'aria-label',
value: v.translationRef('#?components.grid-list.select-row|Select row'),
severity: 'recommended'
},
{
attr: 'aria-disabled',
value: v.literal('true'),
severity: 'optional',
condition: { when: 'prop-truthy', prop: 'disabled' }
}
]
}
]
};

@ -1,11 +1,20 @@
import type { Morfo } from '../types';
import { v } from '../types';
export const linkPreviewMorfo: Morfo = {
name: 'LinkPreview',
kebab: 'link-preview',
scope: ['soma'],
parts: [
{ name: 'Provider', kebab: 'root', kind: 'virtual', defaultElement: 'none', optional: false, data: [], aria: [] },
{
name: 'Provider',
kebab: 'root',
kind: 'virtual',
defaultElement: 'none',
optional: false,
data: [],
aria: []
},
{
name: 'Trigger',
kebab: 'trigger',
@ -24,8 +33,16 @@ export const linkPreviewMorfo: Morfo = {
optional: false,
states: ['open', 'closed'],
data: [{ attr: 'data-state', values: ['open', 'closed'] }],
aria: []
aria: [{ attr: 'aria-hidden', value: v.literal('true') }]
},
{ name: 'Arrow', kebab: 'arrow', kind: 'public', defaultElement: 'div', optional: true, data: [], aria: [] }
{
name: 'Arrow',
kebab: 'arrow',
kind: 'public',
defaultElement: 'div',
optional: true,
data: [],
aria: []
}
]
};

@ -1,4 +1,5 @@
import type { Morfo } from '../types';
import { v } from '../types';
export const listboxMorfo: Morfo = {
name: 'Listbox',
@ -11,6 +12,7 @@ export const listboxMorfo: Morfo = {
kebab: 'root',
kind: 'public',
defaultElement: 'div',
role: 'listbox',
optional: false,
data: [
{ attr: 'data-orientation', values: ['horizontal', 'vertical'] },
@ -21,13 +23,59 @@ export const listboxMorfo: Morfo = {
{ attr: 'data-focused', severity: 'optional' },
{ attr: 'data-empty', severity: 'optional' }
],
aria: []
aria: [
{ attr: 'aria-orientation', value: v.propRef('orientation') },
{
attr: 'aria-multiselectable',
value: v.literal('true'),
severity: 'optional',
condition: { when: 'prop-truthy', prop: 'multiple' }
},
{
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-required',
value: v.literal('true'),
severity: 'optional',
condition: { when: 'prop-truthy', prop: 'required' }
},
{
attr: 'aria-invalid',
value: v.literal('true'),
severity: 'optional',
condition: { when: 'prop-truthy', prop: 'invalid' }
},
{
attr: 'aria-label',
value: v.translationRef('#?components.listbox.label|Options'),
severity: 'recommended'
}
],
keyboard: [
{ key: 'ArrowDown', action: 'next-item' },
{ key: 'ArrowUp', action: 'prev-item' },
{ key: 'Home', action: 'first-item' },
{ key: 'End', action: 'last-item' },
{ key: 'Enter', action: 'select' },
{ key: ' ', action: 'select' }
]
},
{
name: 'Item',
kebab: 'item',
kind: 'public',
defaultElement: 'div',
role: 'option',
optional: false,
states: ['selected', 'unselected'],
data: [
@ -36,10 +84,50 @@ export const listboxMorfo: Morfo = {
{ attr: 'data-disabled', severity: 'optional' },
{ attr: 'data-orientation', values: ['horizontal', 'vertical'] }
],
aria: []
aria: [
{ attr: 'aria-selected', value: v.stateRef('selected') },
{
attr: 'aria-disabled',
value: v.literal('true'),
severity: 'optional',
condition: { when: 'prop-truthy', prop: 'disabled' }
}
]
},
{
name: 'ItemIndicator',
kebab: 'item-indicator',
kind: 'public',
defaultElement: 'span',
optional: true,
data: [],
aria: [{ attr: 'aria-hidden', value: v.literal('true') }]
},
{
name: 'Group',
kebab: 'group',
kind: 'public',
defaultElement: 'div',
role: 'group',
optional: true,
data: [],
aria: [
{
attr: 'aria-labelledby',
value: v.partRef('group-label'),
condition: { when: 'part-present', part: 'group-label' },
severity: 'recommended'
}
]
},
{ name: 'ItemIndicator', kebab: 'item-indicator', kind: 'public', defaultElement: 'span', optional: true, data: [], aria: [] },
{ name: 'Group', kebab: 'group', kind: 'public', defaultElement: 'div', optional: true, data: [], aria: [] },
{ name: 'GroupLabel', kebab: 'group-label', kind: 'public', defaultElement: 'div', optional: true, data: [], aria: [] }
{
name: 'GroupLabel',
kebab: 'group-label',
kind: 'public',
defaultElement: 'div',
optional: true,
data: [],
aria: []
}
]
};

@ -1,4 +1,5 @@
import type { Morfo } from '../types';
import { v } from '../types';
export const menubarMorfo: Morfo = {
name: 'Menubar',
@ -6,12 +7,22 @@ export const menubarMorfo: Morfo = {
scope: ['soma'],
apg: 'https://www.w3.org/WAI/ARIA/apg/patterns/menubar/',
parts: [
{ name: 'Provider', kebab: 'root', kind: 'public', defaultElement: 'div', optional: false, data: [], aria: [] },
{
name: 'Provider',
kebab: 'root',
kind: 'public',
defaultElement: 'div',
role: 'menubar',
optional: false,
data: [],
aria: [{ attr: 'aria-orientation', value: v.literal('horizontal') }]
},
{
name: 'Trigger',
kebab: 'trigger',
kind: 'public',
defaultElement: 'button',
role: 'menuitem',
optional: false,
states: ['open', 'closed'],
data: [
@ -20,7 +31,20 @@ export const menubarMorfo: Morfo = {
{ attr: 'data-highlighted', severity: 'optional' },
{ attr: 'data-menubar-value' }
],
aria: []
aria: [
{ attr: 'type', value: v.literal('button') },
{ attr: 'aria-haspopup', value: v.literal('menu') },
{ attr: 'aria-expanded', value: v.stateRef('open') }
],
keyboard: [
{ key: 'Enter', action: 'open' },
{ key: ' ', action: 'open' },
{ key: 'ArrowDown', action: 'open' },
{ key: 'ArrowRight', action: 'next-item' },
{ key: 'ArrowLeft', action: 'prev-item' },
{ key: 'Home', action: 'first-item' },
{ key: 'End', action: 'last-item' }
]
}
]
};

@ -1,4 +1,5 @@
import type { Morfo } from '../types';
import { v } from '../types';
export const meterMorfo: Morfo = {
name: 'Meter',
@ -10,6 +11,7 @@ export const meterMorfo: Morfo = {
kebab: 'root',
kind: 'public',
defaultElement: 'div',
role: 'meter',
optional: false,
states: ['below', 'optimum', 'above'],
data: [
@ -18,7 +20,17 @@ export const meterMorfo: Morfo = {
{ attr: 'data-min' },
{ attr: 'data-max' }
],
aria: []
aria: [
{ attr: 'aria-valuemin', value: v.propRef('min') },
{ attr: 'aria-valuemax', value: v.propRef('max') },
{ attr: 'aria-valuenow', value: v.propRef('value') },
{ attr: 'aria-valuetext', value: v.propRef('valueText'), severity: 'recommended' },
{
attr: 'aria-label',
value: v.translationRef('#?components.meter.label|Meter'),
severity: 'recommended'
}
]
},
{
name: 'Indicator',

@ -1,4 +1,5 @@
import type { Morfo } from '../types';
import { v } from '../types';
export const navigationMenuMorfo: Morfo = {
name: 'NavigationMenu',
@ -21,7 +22,19 @@ export const navigationMenuMorfo: Morfo = {
defaultElement: 'ul',
optional: false,
data: [{ attr: 'data-orientation', values: ['horizontal', 'vertical'] }],
aria: []
aria: [
{
attr: 'aria-label',
value: v.translationRef('#?components.navigation-menu.label|Main navigation'),
severity: 'recommended'
},
{ attr: 'aria-orientation', value: v.propRef('orientation') }
],
keyboard: [
{ key: 'Home', action: 'first-item' },
{ key: 'End', action: 'last-item' },
{ key: 'Escape', action: 'close' }
]
},
{
name: 'Item',
@ -41,13 +54,28 @@ export const navigationMenuMorfo: Morfo = {
kebab: 'trigger',
kind: 'public',
defaultElement: 'button',
role: 'button',
optional: true,
states: ['open', 'closed'],
data: [
{ attr: 'data-state', values: ['open', 'closed'] },
{ attr: 'data-disabled', severity: 'optional' }
],
aria: []
aria: [
{ attr: 'type', value: v.literal('button') },
{ attr: 'aria-expanded', value: v.stateRef('open') },
{
attr: 'aria-controls',
value: v.partRef('content'),
condition: { when: 'part-present', part: 'content' },
severity: 'recommended'
}
],
keyboard: [
{ key: 'Enter', action: 'open' },
{ key: ' ', action: 'open' },
{ key: 'ArrowDown', action: 'open' }
]
},
{
name: 'Content',
@ -60,7 +88,15 @@ export const navigationMenuMorfo: Morfo = {
{ attr: 'data-state', values: ['open', 'closed'] },
{ attr: 'data-motion', values: ['from-start', 'from-end', 'to-start', 'to-end'] }
],
aria: []
aria: [
{
attr: 'aria-labelledby',
value: v.partRef('trigger'),
condition: { when: 'part-present', part: 'trigger' },
severity: 'recommended'
}
],
keyboard: [{ key: 'Escape', action: 'close' }]
},
{
name: 'Link',
@ -69,7 +105,14 @@ export const navigationMenuMorfo: Morfo = {
defaultElement: 'a',
optional: true,
data: [{ attr: 'data-active', severity: 'optional' }],
aria: []
aria: [
{
attr: 'aria-current',
value: v.literal('page'),
severity: 'optional',
condition: { when: 'prop-truthy', prop: 'active' }
}
]
},
{
name: 'Indicator',

@ -1,4 +1,5 @@
import type { Morfo } from '../types';
import { v } from '../types';
export const numberFieldMorfo: Morfo = {
name: 'NumberField',
@ -15,8 +16,10 @@ export const numberFieldMorfo: Morfo = {
data: [
{ attr: 'data-disabled', severity: 'optional' },
{ attr: 'data-readonly', severity: 'optional' },
{ attr: 'data-required', severity: 'optional' },
{ attr: 'data-invalid', severity: 'optional' },
{ attr: 'data-focused', severity: 'optional' }
{ attr: 'data-focused', severity: 'optional' },
{ attr: 'data-scrubbing', severity: 'optional' }
],
aria: []
},
@ -25,43 +28,103 @@ export const numberFieldMorfo: Morfo = {
kebab: 'input',
kind: 'public',
defaultElement: 'input',
role: 'spinbutton',
optional: false,
data: [
{ attr: 'data-disabled', severity: 'optional' },
{ attr: 'data-readonly', severity: 'optional' },
{ attr: 'data-invalid', severity: 'optional' }
],
aria: []
aria: [
{ attr: 'aria-valuenow', value: v.propRef('value') },
{
attr: 'aria-valuemin',
value: v.propRef('min'),
severity: 'optional',
condition: { when: 'prop-truthy', prop: 'min' }
},
{
attr: 'aria-valuemax',
value: v.propRef('max'),
severity: 'optional',
condition: { when: 'prop-truthy', 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' }
],
keyboard: [
{ key: 'ArrowUp', action: 'increment' },
{ key: 'ArrowDown', action: 'decrement' },
{ key: 'Shift+ArrowUp', action: 'increment-large' },
{ key: 'Shift+ArrowDown', action: 'decrement-large' },
{ key: 'PageUp', action: 'increment-large' },
{ key: 'PageDown', action: 'decrement-large' },
{ key: 'Home', action: 'first-item' },
{ key: 'End', action: 'last-item' },
{ key: 'Enter', action: 'commit' }
]
},
{
name: 'IncrementTrigger',
kebab: 'increment-trigger',
kind: 'public',
defaultElement: 'button',
role: 'button',
optional: true,
data: [{ attr: 'data-disabled', severity: 'optional' }],
aria: []
aria: [
{ attr: 'type', value: v.literal('button') },
{
attr: 'aria-label',
value: v.translationRef('#?common.buttons.increment|Increment'),
severity: 'recommended'
},
{
attr: 'aria-disabled',
value: v.literal('true'),
severity: 'optional',
condition: { when: 'prop-truthy', prop: 'disabled' }
}
]
},
{
name: 'DecrementTrigger',
kebab: 'decrement-trigger',
kind: 'public',
defaultElement: 'button',
role: 'button',
optional: true,
data: [{ attr: 'data-disabled', severity: 'optional' }],
aria: []
aria: [
{ attr: 'type', value: v.literal('button') },
{
attr: 'aria-label',
value: v.translationRef('#?common.buttons.decrement|Decrement'),
severity: 'recommended'
},
{
attr: 'aria-disabled',
value: v.literal('true'),
severity: 'optional',
condition: { when: 'prop-truthy', prop: 'disabled' }
}
]
},
{
name: 'Scrubber',
kebab: 'scrubber',
kind: 'public',
defaultElement: 'div',
role: 'presentation',
optional: true,
data: [
{ attr: 'data-scrubbing', severity: 'optional' },
{ attr: 'data-disabled', severity: 'optional' }
],
aria: []
aria: [{ attr: 'aria-hidden', value: v.literal('true') }]
}
]
};

@ -1,4 +1,5 @@
import type { Morfo } from '../types';
import { v } from '../types';
export const paginationMorfo: Morfo = {
name: 'Pagination',
@ -10,39 +11,72 @@ export const paginationMorfo: Morfo = {
kebab: 'root',
kind: 'public',
defaultElement: 'nav',
role: 'navigation',
optional: false,
data: [{ attr: 'data-disabled', severity: 'optional' }],
aria: []
aria: [
{
attr: 'aria-label',
value: v.translationRef('#?components.pagination.label|Pagination'),
severity: 'recommended'
}
]
},
{
name: 'PrevTrigger',
kebab: 'prev-trigger',
kind: 'public',
defaultElement: 'button',
role: 'button',
optional: true,
data: [{ attr: 'data-disabled', severity: 'optional' }],
aria: []
aria: [
{ attr: 'type', value: v.literal('button') },
{
attr: 'aria-label',
value: v.translationRef('#?components.pagination.prev|Previous page'),
severity: 'recommended'
}
]
},
{
name: 'NextTrigger',
kebab: 'next-trigger',
kind: 'public',
defaultElement: 'button',
role: 'button',
optional: true,
data: [{ attr: 'data-disabled', severity: 'optional' }],
aria: []
aria: [
{ attr: 'type', value: v.literal('button') },
{
attr: 'aria-label',
value: v.translationRef('#?components.pagination.next|Next page'),
severity: 'recommended'
}
]
},
{
name: 'Item',
kebab: 'item',
kind: 'public',
defaultElement: 'button',
role: 'button',
optional: false,
data: [
{ attr: 'data-selected', severity: 'optional' },
{ attr: 'data-disabled', severity: 'optional' }
],
aria: []
aria: [
{ attr: 'type', value: v.literal('button') },
{
attr: 'aria-current',
value: v.literal('page'),
severity: 'optional',
condition: { when: 'prop-truthy', prop: 'selected' }
},
{ attr: 'aria-label', value: v.propRef('value'), severity: 'recommended' }
]
},
{
name: 'Ellipsis',
@ -51,7 +85,7 @@ export const paginationMorfo: Morfo = {
defaultElement: 'span',
optional: true,
data: [{ attr: 'data-position', values: ['start', 'end'] }],
aria: []
aria: [{ attr: 'aria-hidden', value: v.literal('true') }]
}
]
};

@ -1,4 +1,5 @@
import type { Morfo } from '../types';
import { v } from '../types';
export const pinInputMorfo: Morfo = {
name: 'PinInput',
@ -31,13 +32,27 @@ export const pinInputMorfo: Morfo = {
{ attr: 'data-disabled', severity: 'optional' },
{ attr: 'data-readonly', severity: 'optional' }
],
aria: []
aria: [
{ attr: 'type', value: v.literal('text') },
{
attr: 'aria-label',
value: v.translationRef('#?components.pin-input.input|Verification code'),
severity: 'recommended'
},
{
attr: 'aria-invalid',
value: v.literal('true'),
severity: 'optional',
condition: { when: 'prop-truthy', prop: 'invalid' }
}
]
},
{
name: 'Cell',
kebab: 'cell',
kind: 'public',
defaultElement: 'div',
role: 'presentation',
optional: false,
data: [
{ attr: 'data-active', severity: 'optional' },
@ -46,7 +61,7 @@ export const pinInputMorfo: Morfo = {
{ attr: 'data-invalid', severity: 'optional' },
{ attr: 'data-placeholder', severity: 'optional' }
],
aria: []
aria: [{ attr: 'aria-hidden', value: v.literal('true') }]
}
]
};

@ -1,35 +1,118 @@
import type { Morfo } from '../types';
import { v } from '../types';
export const popoverMorfo: Morfo = {
name: 'Popover',
kebab: 'popover',
scope: ['soma'],
apg: 'https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/',
focus: {
initial: 'first-focusable',
trap: true,
return: 'trigger',
restore: true
},
parts: [
{ name: 'Provider', kebab: 'root', kind: 'virtual', defaultElement: 'none', optional: false, data: [], aria: [] },
{
name: 'Provider',
kebab: 'root',
kind: 'virtual',
defaultElement: 'none',
optional: false,
data: [],
aria: []
},
{
name: 'Trigger',
kebab: 'trigger',
kind: 'public',
defaultElement: 'button',
role: 'button',
optional: false,
states: ['open', 'closed'],
data: [{ attr: 'data-state', values: ['open', 'closed'] }],
aria: []
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'),
condition: { when: 'part-present', part: 'content' },
severity: 'recommended'
}
],
keyboard: [
{ key: 'Enter', action: 'open' },
{ key: ' ', action: 'open' }
]
},
{
name: 'Content',
kebab: 'content',
kind: 'public',
defaultElement: 'div',
role: 'dialog',
optional: false,
states: ['open', 'closed'],
data: [{ attr: 'data-state', values: ['open', 'closed'] }],
aria: [
{
attr: 'aria-labelledby',
value: v.partRef('trigger'),
condition: { when: 'part-present', part: 'trigger' },
severity: 'recommended'
},
{
attr: 'aria-modal',
value: v.literal('true'),
severity: 'optional',
condition: { when: 'prop-truthy', prop: 'modal' }
}
],
keyboard: [
{ key: 'Escape', action: 'close' },
{ key: 'Tab', action: 'focus-next' },
{ key: 'Shift+Tab', action: 'focus-prev' }
]
},
{
name: 'Arrow',
kebab: 'arrow',
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.translationRef('#?components.popover.close|Close'),
severity: 'recommended'
}
]
},
{
name: 'Anchor',
kebab: 'anchor',
kind: 'public',
defaultElement: 'div',
optional: true,
data: [],
aria: []
},
{ name: 'Arrow', kebab: 'arrow', kind: 'public', defaultElement: 'div', optional: true, data: [], aria: [] },
{ name: 'Close', kebab: 'close', kind: 'public', defaultElement: 'button', optional: true, data: [], aria: [] },
{ name: 'Anchor', kebab: 'anchor', kind: 'public', defaultElement: 'div', optional: true, data: [], aria: [] },
{
name: 'Overlay',
kebab: 'overlay',
@ -38,7 +121,7 @@ export const popoverMorfo: Morfo = {
optional: true,
states: ['open', 'closed'],
data: [{ attr: 'data-state', values: ['open', 'closed'] }],
aria: []
aria: [{ attr: 'aria-hidden', value: v.literal('true') }]
}
]
};

@ -1,4 +1,5 @@
import type { Morfo } from '../types';
import { v } from '../types';
export const progressMorfo: Morfo = {
name: 'Progress',
@ -10,6 +11,7 @@ export const progressMorfo: Morfo = {
kebab: 'root',
kind: 'public',
defaultElement: 'div',
role: 'progressbar',
optional: false,
states: ['indeterminate', 'loading', 'loaded'],
data: [
@ -18,7 +20,22 @@ export const progressMorfo: Morfo = {
{ attr: 'data-max' },
{ attr: 'data-min' }
],
aria: []
aria: [
{ attr: 'aria-valuemin', value: v.propRef('min') },
{ attr: 'aria-valuemax', value: v.propRef('max') },
{
attr: 'aria-valuenow',
value: v.propRef('value'),
severity: 'optional',
condition: { when: 'prop-truthy', prop: 'value' }
},
{ attr: 'aria-valuetext', value: v.propRef('valueText'), severity: 'recommended' },
{
attr: 'aria-label',
value: v.translationRef('#?components.progress.label|Loading'),
severity: 'recommended'
}
]
},
{
name: 'Indicator',

@ -1,4 +1,5 @@
import type { Morfo } from '../types';
import { v } from '../types';
export const radioGroupMorfo: Morfo = {
name: 'RadioGroup',
@ -11,18 +12,35 @@ export const radioGroupMorfo: Morfo = {
kebab: 'root',
kind: 'public',
defaultElement: 'div',
role: 'radiogroup',
optional: false,
data: [
{ attr: 'data-orientation', values: ['horizontal', 'vertical'] },
{ attr: 'data-disabled', severity: 'optional' }
{ attr: 'data-disabled', severity: 'optional' },
{ attr: 'data-readonly', severity: 'optional' }
],
aria: []
aria: [
{ attr: 'aria-orientation', value: v.propRef('orientation') },
{
attr: 'aria-required',
value: v.literal('true'),
severity: 'optional',
condition: { when: 'prop-truthy', prop: 'required' }
},
{
attr: 'aria-readonly',
value: v.literal('true'),
severity: 'optional',
condition: { when: 'prop-truthy', prop: 'readonly' }
}
]
},
{
name: 'Item',
kebab: 'item',
kind: 'public',
defaultElement: 'button',
role: 'radio',
optional: false,
states: ['checked', 'unchecked'],
data: [
@ -30,7 +48,16 @@ export const radioGroupMorfo: Morfo = {
{ attr: 'data-disabled', severity: 'optional' },
{ attr: 'data-orientation', values: ['horizontal', 'vertical'] }
],
aria: []
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' }
]
}
]
};

@ -1,4 +1,5 @@
import type { Morfo } from '../types';
import { v } from '../types';
export const rangeCalendarMorfo: Morfo = {
name: 'RangeCalendar',
@ -11,6 +12,7 @@ export const rangeCalendarMorfo: Morfo = {
kebab: 'root',
kind: 'public',
defaultElement: 'div',
role: 'application',
optional: false,
data: [
{ attr: 'data-readonly', severity: 'optional' },
@ -18,27 +20,75 @@ export const rangeCalendarMorfo: Morfo = {
{ attr: 'data-invalid', severity: 'optional' },
{ attr: 'data-selecting', severity: 'optional' }
],
aria: [
{ attr: 'aria-label', value: v.propRef('ariaLabel'), severity: 'recommended' },
{ attr: 'aria-disabled', value: v.propRef('disabled'), severity: 'optional' }
],
keyboard: [
{ key: 'ArrowRight', action: 'next-day' },
{ key: 'ArrowLeft', action: 'prev-day' },
{ key: 'ArrowDown', action: 'next-week' },
{ key: 'ArrowUp', action: 'prev-week' },
{ key: 'PageUp', action: 'prev-month' },
{ key: 'PageDown', action: 'next-month' },
{ key: 'Home', action: 'first-day-of-week' },
{ key: 'End', action: 'last-day-of-week' },
{ key: 'Enter', action: 'select' },
{ key: ' ', action: 'select' },
{ key: 'Escape', action: 'cancel-selection' }
]
},
{
name: 'Header',
kebab: 'header',
kind: 'public',
defaultElement: 'div',
optional: true,
data: [],
aria: [{ attr: 'aria-hidden', value: v.literal('true') }]
},
{
name: 'Heading',
kebab: 'heading',
kind: 'public',
defaultElement: 'div',
optional: true,
data: [],
aria: []
},
{ name: 'Header', kebab: 'header', kind: 'public', defaultElement: 'div', optional: true, data: [], aria: [] },
{ name: 'Heading', kebab: 'heading', kind: 'public', defaultElement: 'div', optional: true, data: [], aria: [] },
{
name: 'PrevButton',
kebab: 'prev-button',
kind: 'public',
defaultElement: 'button',
role: 'button',
optional: true,
data: [{ attr: 'data-disabled', severity: 'optional' }],
aria: []
aria: [
{ attr: 'type', value: v.literal('button') },
{
attr: 'aria-label',
value: v.translationRef('#?components.range-calendar.prev-month|Previous month'),
severity: 'recommended'
}
]
},
{
name: 'NextButton',
kebab: 'next-button',
kind: 'public',
defaultElement: 'button',
role: 'button',
optional: true,
data: [{ attr: 'data-disabled', severity: 'optional' }],
aria: []
aria: [
{ attr: 'type', value: v.literal('button') },
{
attr: 'aria-label',
value: v.translationRef('#?components.range-calendar.next-month|Next month'),
severity: 'recommended'
}
]
},
{
name: 'MonthSelect',
@ -58,16 +108,68 @@ export const rangeCalendarMorfo: Morfo = {
data: [{ attr: 'data-disabled', severity: 'optional' }],
aria: []
},
{ name: 'Grid', kebab: 'grid', kind: 'public', defaultElement: 'table', optional: false, data: [], aria: [] },
{ name: 'GridHead', kebab: 'grid-head', kind: 'public', defaultElement: 'thead', optional: false, data: [], aria: [] },
{ name: 'GridBody', kebab: 'grid-body', kind: 'public', defaultElement: 'tbody', optional: false, data: [], aria: [] },
{ name: 'GridRow', kebab: 'grid-row', kind: 'public', defaultElement: 'tr', optional: false, data: [], aria: [] },
{ name: 'HeadCell', kebab: 'head-cell', kind: 'public', defaultElement: 'th', optional: false, data: [], aria: [] },
{
name: 'Grid',
kebab: 'grid',
kind: 'public',
defaultElement: 'table',
role: 'grid',
optional: false,
data: [],
aria: [
{
attr: 'aria-labelledby',
value: v.partRef('heading'),
condition: { when: 'part-present', part: 'heading' },
severity: 'recommended'
},
{ attr: 'aria-readonly', value: v.propRef('readonly'), severity: 'optional' },
{ attr: 'aria-disabled', value: v.propRef('disabled'), severity: 'optional' }
]
},
{
name: 'GridHead',
kebab: 'grid-head',
kind: 'public',
defaultElement: 'thead',
optional: false,
data: [],
aria: []
},
{
name: 'GridBody',
kebab: 'grid-body',
kind: 'public',
defaultElement: 'tbody',
optional: false,
data: [],
aria: []
},
{
name: 'GridRow',
kebab: 'grid-row',
kind: 'public',
defaultElement: 'tr',
role: 'row',
optional: false,
data: [],
aria: []
},
{
name: 'HeadCell',
kebab: 'head-cell',
kind: 'public',
defaultElement: 'th',
optional: false,
data: [],
aria: []
},
{
name: 'Cell',
kebab: 'cell',
kind: 'public',
defaultElement: 'td',
role: 'gridcell',
optional: false,
data: [
{ attr: 'data-selected', severity: 'optional' },
@ -84,13 +186,17 @@ export const rangeCalendarMorfo: Morfo = {
{ attr: 'data-holiday', severity: 'optional' },
{ attr: 'data-outside-month', severity: 'optional' }
],
aria: []
aria: [
{ attr: 'aria-selected', value: v.propRef('selected'), severity: 'optional' },
{ attr: 'aria-disabled', value: v.propRef('disabled'), severity: 'optional' }
]
},
{
name: 'Day',
kebab: 'day',
kind: 'public',
defaultElement: 'div',
role: 'button',
optional: false,
data: [
{ attr: 'data-selected', severity: 'optional' },
@ -108,7 +214,11 @@ export const rangeCalendarMorfo: Morfo = {
{ attr: 'data-outside-month', severity: 'optional' },
{ attr: 'data-focused', severity: 'optional' }
],
aria: []
aria: [
{ attr: 'aria-label', value: v.propRef('dateLabel'), severity: 'recommended' },
{ attr: 'aria-disabled', value: v.propRef('disabled'), severity: 'optional' },
{ attr: 'aria-selected', value: v.propRef('selected'), severity: 'optional' }
]
}
]
};

@ -1,4 +1,5 @@
import type { Morfo } from '../types';
import { v } from '../types';
export const ratingGroupMorfo: Morfo = {
name: 'RatingGroup',
@ -10,6 +11,7 @@ export const ratingGroupMorfo: Morfo = {
kebab: 'root',
kind: 'public',
defaultElement: 'div',
role: 'slider',
optional: false,
data: [
{ attr: 'data-disabled', severity: 'optional' },
@ -19,13 +21,56 @@ export const ratingGroupMorfo: Morfo = {
{ attr: 'data-focused', severity: 'optional' },
{ attr: 'data-orientation', values: ['horizontal', 'vertical'] }
],
aria: []
aria: [
{ attr: 'aria-valuenow', value: v.propRef('value') },
{ attr: 'aria-valuemin', value: v.propRef('min') },
{ attr: 'aria-valuemax', value: v.propRef('max') },
{ attr: 'aria-orientation', value: v.propRef('orientation') },
{
attr: 'aria-label',
value: v.translationRef('#?components.rating-group.label|Rating'),
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' }
}
],
keyboard: [
{ key: 'ArrowRight', action: 'increment' },
{ key: 'ArrowUp', action: 'increment' },
{ key: 'ArrowLeft', action: 'decrement' },
{ key: 'ArrowDown', action: 'decrement' },
{ key: 'Home', action: 'first-item' },
{ key: 'End', action: 'last-item' }
]
},
{
name: 'Item',
kebab: 'item',
kind: 'public',
defaultElement: 'div',
role: 'presentation',
optional: false,
states: ['active', 'partial', 'inactive'],
data: [

@ -1,4 +1,5 @@
import type { Morfo } from '../types';
import { v } from '../types';
export const scrollAreaMorfo: Morfo = {
name: 'ScrollArea',
@ -36,6 +37,7 @@ export const scrollAreaMorfo: Morfo = {
kebab: 'scrollbar',
kind: 'public',
defaultElement: 'div',
role: 'scrollbar',
optional: true,
states: ['visible', 'hidden'],
data: [
@ -44,7 +46,23 @@ export const scrollAreaMorfo: Morfo = {
{ attr: 'data-hover', severity: 'optional' },
{ attr: 'data-dragging', severity: 'optional' }
],
aria: []
aria: [
{
attr: 'aria-controls',
value: v.partRef('viewport'),
severity: 'optional',
condition: { when: 'part-present', part: 'viewport' }
},
{ attr: 'aria-valuenow', value: v.propRef('scrollPercent') },
{ attr: 'aria-valuemin', value: v.literal('0') },
{ attr: 'aria-valuemax', value: v.literal('100') },
{ attr: 'aria-orientation', value: v.propRef('orientation') },
{
attr: 'aria-label',
value: v.translationRef('#?components.scroll-area.scrollbar-vertical|Vertical scrollbar'),
severity: 'recommended'
}
]
},
{
name: 'Thumb',

@ -1,4 +1,5 @@
import type { Morfo } from '../types';
import { v } from '../types';
export const searchFieldMorfo: Morfo = {
name: 'SearchField',
@ -26,21 +27,51 @@ export const searchFieldMorfo: Morfo = {
kebab: 'input',
kind: 'public',
defaultElement: 'input',
role: 'searchbox',
optional: false,
data: [
{ attr: 'data-disabled', severity: 'optional' },
{ attr: 'data-readonly', severity: 'optional' }
],
aria: []
aria: [
{ attr: 'type', value: v.literal('search') },
{
attr: 'aria-label',
value: v.translationRef('#?components.search-field.label|Search'),
severity: 'recommended'
},
{
attr: 'aria-invalid',
value: v.literal('true'),
severity: 'optional',
condition: { when: 'prop-truthy', prop: 'invalid' }
}
],
keyboard: [
{ key: 'Enter', action: 'submit' },
{
key: 'Escape',
action: 'clear',
condition: { when: 'prop-truthy', prop: 'clearOnEscape' }
}
]
},
{
name: 'ClearTrigger',
kebab: 'clear-trigger',
kind: 'public',
defaultElement: 'button',
role: 'button',
optional: true,
data: [{ attr: 'data-disabled', severity: 'optional' }],
aria: []
aria: [
{ attr: 'type', value: v.literal('button') },
{
attr: 'aria-label',
value: v.translationRef('#?components.search-field.clear|Clear search'),
severity: 'recommended'
}
]
}
]
};

@ -1,17 +1,35 @@
import type { Morfo } from '../types';
import { v } from '../types';
export const selectMorfo: Morfo = {
name: 'Select',
kebab: 'select',
scope: ['soma'],
apg: 'https://www.w3.org/WAI/ARIA/apg/patterns/combobox/',
focus: {
initial: 'first-focusable',
trap: true,
return: 'trigger',
restore: true
},
parts: [
{ name: 'Provider', kebab: 'root', kind: 'virtual', defaultElement: 'none', optional: false, data: [], aria: [] },
{
name: 'Provider',
kebab: 'root',
kind: 'virtual',
defaultElement: 'none',
optional: false,
data: [],
aria: []
},
{
name: 'Trigger',
kebab: 'trigger',
kind: 'public',
defaultElement: 'button',
role: 'combobox',
optional: false,
states: ['open', 'closed'],
data: [
@ -19,36 +37,142 @@ export const selectMorfo: Morfo = {
{ attr: 'data-disabled', severity: 'optional' },
{ attr: 'data-placeholder', severity: 'optional' }
],
aria: [
{ attr: 'type', value: v.literal('button') },
{ attr: 'aria-haspopup', value: v.literal('listbox') },
{ attr: 'aria-expanded', value: v.stateRef('open') },
{
attr: 'aria-controls',
value: v.partRef('content'),
condition: { when: 'part-present', part: 'content' },
severity: 'recommended'
},
{ attr: 'aria-activedescendant', value: v.propRef('highlightedId'), severity: 'optional' },
{
attr: 'aria-required',
value: v.literal('true'),
severity: 'optional',
condition: { when: 'prop-truthy', prop: 'required' }
}
],
keyboard: [
{ key: 'Enter', action: 'open' },
{ key: ' ', action: 'open' },
{ key: 'ArrowDown', action: 'next-item' },
{ key: 'ArrowUp', action: 'prev-item' },
{ key: 'Home', action: 'first-item' },
{ key: 'End', action: 'last-item' },
{ key: 'Escape', action: 'close' }
]
},
{
name: 'Value',
kebab: 'value',
kind: 'public',
defaultElement: 'span',
optional: true,
data: [],
aria: []
},
{ name: 'Value', kebab: 'value', kind: 'public', defaultElement: 'span', optional: true, data: [], aria: [] },
{
name: 'Content',
kebab: 'content',
kind: 'public',
defaultElement: 'div',
role: 'listbox',
optional: false,
states: ['open', 'closed'],
data: [{ attr: 'data-state', values: ['open', 'closed'] }],
aria: [
{
attr: 'aria-labelledby',
value: v.partRef('trigger'),
condition: { when: 'part-present', part: 'trigger' },
severity: 'recommended'
},
{
attr: 'aria-multiselectable',
value: v.literal('true'),
severity: 'optional',
condition: { when: 'prop-truthy', prop: 'multiple' }
}
]
},
{
name: 'Viewport',
kebab: 'viewport',
kind: 'public',
defaultElement: 'div',
optional: true,
data: [],
aria: []
},
{ name: 'Viewport', kebab: 'viewport', kind: 'public', defaultElement: 'div', optional: true, data: [], aria: [] },
{
name: 'Item',
kebab: 'item',
kind: 'public',
defaultElement: 'div',
role: 'option',
optional: false,
states: ['checked', 'unchecked'],
data: [
{ attr: 'data-state', values: ['checked', 'unchecked'] },
{ attr: 'data-disabled', severity: 'optional' }
],
aria: [
{ attr: 'aria-selected', value: v.stateRef('checked') },
{
attr: 'aria-disabled',
value: v.literal('true'),
severity: 'optional',
condition: { when: 'prop-truthy', prop: 'disabled' }
}
]
},
{
name: 'Group',
kebab: 'group',
kind: 'public',
defaultElement: 'div',
role: 'group',
optional: true,
data: [],
aria: [
{
attr: 'aria-labelledby',
value: v.partRef('group-heading'),
condition: { when: 'part-present', part: 'group-heading' },
severity: 'recommended'
}
]
},
{
name: 'GroupHeading',
kebab: 'group-heading',
kind: 'public',
defaultElement: 'div',
optional: true,
data: [],
aria: []
},
{ name: 'Group', kebab: 'group', kind: 'public', defaultElement: 'div', optional: true, data: [], aria: [] },
{ name: 'GroupHeading', kebab: 'group-heading', kind: 'public', defaultElement: 'div', optional: true, data: [], aria: [] },
{ name: 'Separator', kebab: 'separator', kind: 'public', defaultElement: 'div', optional: true, data: [], aria: [] },
{ name: 'Arrow', kebab: 'arrow', kind: 'public', defaultElement: 'div', optional: true, data: [], aria: [] }
{
name: 'Separator',
kebab: 'separator',
kind: 'public',
defaultElement: 'div',
role: 'separator',
optional: true,
data: [],
aria: []
},
{
name: 'Arrow',
kebab: 'arrow',
kind: 'public',
defaultElement: 'div',
optional: true,
data: [],
aria: []
}
]
};

@ -1,4 +1,5 @@
import type { Morfo } from '../types';
import { v } from '../types';
export const sliderMorfo: Morfo = {
name: 'Slider',
@ -11,6 +12,7 @@ export const sliderMorfo: Morfo = {
kebab: 'root',
kind: 'public',
defaultElement: 'div',
role: 'group',
optional: false,
data: [
{ attr: 'data-orientation', values: ['horizontal', 'vertical'] },
@ -35,12 +37,40 @@ export const sliderMorfo: Morfo = {
kebab: 'thumb',
kind: 'public',
defaultElement: 'div',
role: 'slider',
optional: false,
data: [
{ attr: 'data-orientation', values: ['horizontal', 'vertical'] },
{ attr: 'data-disabled', severity: 'optional' }
],
aria: []
aria: [
{ attr: 'aria-valuemin', value: v.propRef('min') },
{ attr: 'aria-valuemax', value: v.propRef('max') },
{ attr: 'aria-valuenow', value: v.propRef('value') },
{ attr: 'aria-orientation', value: v.propRef('orientation') },
{
attr: 'aria-label',
value: v.propRef('thumbLabels'),
severity: 'optional',
condition: { when: 'prop-truthy', prop: 'thumbLabels' }
},
{
attr: 'aria-disabled',
value: v.literal('true'),
severity: 'optional',
condition: { when: 'prop-truthy', prop: 'disabled' }
}
],
keyboard: [
{ key: 'ArrowUp', action: 'increment' },
{ key: 'ArrowDown', action: 'decrement' },
{ key: 'ArrowRight', action: 'increment' },
{ key: 'ArrowLeft', action: 'decrement' },
{ key: 'PageUp', action: 'increment-large' },
{ key: 'PageDown', action: 'decrement-large' },
{ key: 'Home', action: 'first-item' },
{ key: 'End', action: 'last-item' }
]
},
{
name: 'Tick',

@ -1,4 +1,5 @@
import type { Morfo } from '../types';
import { v } from '../types';
export const splitterMorfo: Morfo = {
name: 'Splitter',
@ -13,7 +14,8 @@ export const splitterMorfo: Morfo = {
optional: false,
data: [
{ attr: 'data-orientation', values: ['horizontal', 'vertical'] },
{ attr: 'data-disabled', severity: 'optional' }
{ attr: 'data-disabled', severity: 'optional' },
{ attr: 'data-resizing', severity: 'optional' }
],
aria: []
},
@ -36,6 +38,7 @@ export const splitterMorfo: Morfo = {
kebab: 'resize-trigger',
kind: 'public',
defaultElement: 'div',
role: 'separator',
optional: false,
states: ['dragging', 'hover', 'inactive'],
data: [
@ -44,7 +47,26 @@ export const splitterMorfo: Morfo = {
{ attr: 'data-disabled', severity: 'optional' },
{ attr: 'data-dragging', severity: 'optional' }
],
aria: []
aria: [
{
attr: 'aria-label',
value: v.translationRef('#?components.splitter.resize|Resize panels'),
severity: 'recommended'
},
{ attr: 'aria-orientation', value: v.propRef('orientation') },
{ attr: 'aria-valuenow', value: v.propRef('size') },
{ attr: 'aria-valuemin', value: v.propRef('minSize') },
{ attr: 'aria-valuemax', value: v.propRef('maxSize') }
],
keyboard: [
{ key: 'ArrowRight', action: 'resize' },
{ key: 'ArrowLeft', action: 'resize' },
{ key: 'ArrowUp', action: 'resize' },
{ key: 'ArrowDown', action: 'resize' },
{ key: 'Home', action: 'minimize' },
{ key: 'End', action: 'maximize' },
{ key: 'Enter', action: 'toggle' }
]
}
]
};

@ -1,4 +1,5 @@
import type { Morfo } from '../types';
import { v } from '../types';
export const stepperMorfo: Morfo = {
name: 'Stepper',
@ -17,7 +18,23 @@ export const stepperMorfo: Morfo = {
],
aria: []
},
{ name: 'List', kebab: 'list', kind: 'public', defaultElement: 'ol', optional: false, data: [], aria: [] },
{
name: 'List',
kebab: 'list',
kind: 'public',
defaultElement: 'ol',
role: 'tablist',
optional: false,
data: [],
aria: [
{
attr: 'aria-label',
value: v.translationRef('#?components.stepper.list|Steps'),
severity: 'recommended'
},
{ attr: 'aria-orientation', value: v.propRef('orientation') }
]
},
{
name: 'Item',
kebab: 'item',
@ -38,6 +55,7 @@ export const stepperMorfo: Morfo = {
kebab: 'trigger',
kind: 'public',
defaultElement: 'button',
role: 'tab',
optional: true,
states: ['complete', 'current', 'incomplete'],
data: [
@ -46,7 +64,35 @@ export const stepperMorfo: Morfo = {
{ attr: 'data-complete', severity: 'optional' },
{ attr: 'data-incomplete', severity: 'optional' }
],
aria: []
aria: [
{ attr: 'type', value: v.literal('button') },
{
attr: 'aria-controls',
value: v.partRef('content'),
condition: { when: 'part-present', part: 'content' },
severity: 'recommended'
},
{
attr: 'aria-current',
value: v.literal('step'),
severity: 'optional',
condition: { when: 'state-equals', state: 'current', value: 'true' }
},
{
attr: 'aria-disabled',
value: v.literal('true'),
severity: 'optional',
condition: { when: 'prop-truthy', prop: 'disabled' }
}
],
keyboard: [
{ key: 'ArrowRight', action: 'next-item' },
{ key: 'ArrowLeft', action: 'prev-item' },
{ key: 'ArrowDown', action: 'next-item' },
{ key: 'ArrowUp', action: 'prev-item' },
{ key: 'Home', action: 'first-item' },
{ key: 'End', action: 'last-item' }
]
},
{
name: 'Indicator',
@ -56,13 +102,14 @@ export const stepperMorfo: Morfo = {
optional: true,
states: ['complete', 'current', 'incomplete'],
data: [{ attr: 'data-state', values: ['complete', 'current', 'incomplete'] }],
aria: []
aria: [{ attr: 'aria-hidden', value: v.literal('true') }]
},
{
name: 'Separator',
kebab: 'separator',
kind: 'public',
defaultElement: 'div',
role: 'separator',
optional: true,
states: ['complete', 'current', 'incomplete'],
data: [
@ -76,29 +123,40 @@ export const stepperMorfo: Morfo = {
kebab: 'content',
kind: 'public',
defaultElement: 'div',
role: 'tabpanel',
optional: true,
states: ['complete', 'current', 'incomplete'],
data: [{ attr: 'data-state', values: ['complete', 'current', 'incomplete'] }],
aria: []
},
{ name: 'CompletedContent', kebab: 'completed-content', kind: 'public', defaultElement: 'div', optional: true, data: [], aria: [] },
{
name: 'CompletedContent',
kebab: 'completed-content',
kind: 'public',
defaultElement: 'div',
optional: true,
data: [],
aria: []
},
{
name: 'PrevTrigger',
kebab: 'prev-trigger',
kind: 'public',
defaultElement: 'button',
role: 'button',
optional: true,
data: [{ attr: 'data-disabled', severity: 'optional' }],
aria: []
aria: [{ attr: 'type', value: v.literal('button') }]
},
{
name: 'NextTrigger',
kebab: 'next-trigger',
kind: 'public',
defaultElement: 'button',
role: 'button',
optional: true,
data: [{ attr: 'data-disabled', severity: 'optional' }],
aria: []
aria: [{ attr: 'type', value: v.literal('button') }]
}
]
};

@ -1,4 +1,5 @@
import type { Morfo } from '../types';
import { v } from '../types';
export const switchMorfo: Morfo = {
name: 'Switch',
@ -18,7 +19,20 @@ export const switchMorfo: Morfo = {
{ attr: 'data-state', values: ['checked', 'unchecked'] },
{ attr: 'data-disabled', severity: 'optional' }
],
aria: []
aria: [
{ attr: 'type', value: v.literal('button') },
{ attr: 'aria-checked', value: v.stateRef('checked') },
{
attr: 'aria-required',
value: v.literal('true'),
severity: 'optional',
condition: { when: 'prop-truthy', prop: 'required' }
}
],
keyboard: [
{ key: 'Enter', action: 'toggle' },
{ key: ' ', action: 'toggle' }
]
},
{
name: 'Thumb',

@ -1,50 +1,112 @@
import type { Morfo } from '../types';
import { v } from '../types';
export const tableMorfo: Morfo = {
name: 'Table',
kebab: 'table',
scope: ['soma'],
parts: [
{ name: 'Provider', kebab: 'root', kind: 'public', defaultElement: 'table', optional: false, data: [], aria: [] },
{ name: 'Header', kebab: 'header', kind: 'public', defaultElement: 'thead', optional: true, data: [], aria: [] },
{ name: 'Body', kebab: 'body', kind: 'public', defaultElement: 'tbody', optional: false, data: [], aria: [] },
{ name: 'Footer', kebab: 'footer', kind: 'public', defaultElement: 'tfoot', optional: true, data: [], aria: [] },
{
name: 'Provider',
kebab: 'root',
kind: 'public',
defaultElement: 'table',
optional: false,
data: [],
aria: [
{
attr: 'aria-label',
value: v.translationRef('#?components.table.label|Table'),
severity: 'recommended'
}
]
},
{
name: 'Header',
kebab: 'header',
kind: 'public',
defaultElement: 'thead',
optional: true,
data: [],
aria: []
},
{
name: 'Body',
kebab: 'body',
kind: 'public',
defaultElement: 'tbody',
optional: false,
data: [],
aria: []
},
{
name: 'Footer',
kebab: 'footer',
kind: 'public',
defaultElement: 'tfoot',
optional: true,
data: [],
aria: []
},
{
name: 'ColumnHeader',
kebab: 'column-header',
kind: 'public',
defaultElement: 'th',
role: 'columnheader',
optional: true,
data: [
{ attr: 'data-sortable', severity: 'optional' },
{ attr: 'data-sorted', values: ['asc', 'desc'], severity: 'optional' },
{ attr: 'data-pinned', values: ['left', 'right'], severity: 'optional' }
],
aria: []
aria: [
{
attr: 'aria-sort',
value: v.propRef('sortDirection'),
severity: 'optional',
condition: { when: 'prop-truthy', prop: 'sortable' }
}
]
},
{
name: 'Row',
kebab: 'row',
kind: 'public',
defaultElement: 'tr',
role: 'row',
optional: false,
data: [{ attr: 'data-selected', severity: 'optional' }],
aria: []
aria: [
{ attr: 'aria-rowindex', value: v.propRef('index') },
{
attr: 'aria-selected',
value: v.propRef('selected'),
severity: 'optional',
condition: { when: 'prop-truthy', prop: 'selectionEnabled' }
}
],
keyboard: [
{ key: ' ', action: 'select' },
{ key: 'Enter', action: 'select' }
]
},
{
name: 'Cell',
kebab: 'cell',
kind: 'public',
defaultElement: 'td',
role: 'gridcell',
optional: false,
data: [{ attr: 'data-pinned', values: ['left', 'right'], severity: 'optional' }],
aria: []
aria: [{ attr: 'aria-colindex', value: v.propRef('colIndex') }]
},
{
name: 'RowDetail',
kebab: 'row-detail',
kind: 'public',
defaultElement: 'tr',
role: 'row',
optional: true,
states: ['open', 'closed'],
data: [{ attr: 'data-state', values: ['open', 'closed'] }],
@ -55,10 +117,29 @@ export const tableMorfo: Morfo = {
kebab: 'row-detail-trigger',
kind: 'public',
defaultElement: 'button',
role: 'button',
optional: true,
states: ['open', 'closed'],
data: [{ attr: 'data-state', values: ['open', 'closed'] }],
aria: []
aria: [
{ attr: 'type', value: v.literal('button') },
{ attr: 'aria-expanded', value: v.stateRef('open') },
{
attr: 'aria-controls',
value: v.partRef('row-detail'),
condition: { when: 'part-present', part: 'row-detail' },
severity: 'recommended'
},
{
attr: 'aria-label',
value: v.translationRef('#?components.table.toggle-detail|Toggle detail'),
severity: 'recommended'
}
],
keyboard: [
{ key: ' ', action: 'toggle' },
{ key: 'Enter', action: 'toggle' }
]
}
]
};

@ -1,4 +1,5 @@
import type { Morfo } from '../types';
import { v } from '../types';
export const tabsMorfo: Morfo = {
name: 'Tabs',
@ -23,15 +24,17 @@ export const tabsMorfo: Morfo = {
kebab: 'list',
kind: 'public',
defaultElement: 'div',
role: 'tablist',
optional: false,
data: [{ attr: 'data-orientation', values: ['horizontal', 'vertical'] }],
aria: []
aria: [{ attr: 'aria-orientation', value: v.propRef('orientation') }]
},
{
name: 'Trigger',
kebab: 'trigger',
kind: 'public',
defaultElement: 'button',
role: 'tab',
optional: false,
states: ['active', 'inactive'],
data: [
@ -39,20 +42,47 @@ export const tabsMorfo: Morfo = {
{ attr: 'data-disabled', severity: 'optional' },
{ attr: 'data-orientation', values: ['horizontal', 'vertical'] }
],
aria: []
aria: [
{ attr: 'type', value: v.literal('button') },
{ attr: 'aria-selected', value: v.stateRef('active') },
{
attr: 'aria-controls',
value: v.partRef('content'),
condition: { when: 'part-present', part: 'content' },
severity: 'recommended'
}
],
keyboard: [
{ key: 'ArrowRight', action: 'next-item' },
{ key: 'ArrowLeft', action: 'prev-item' },
{ key: 'ArrowDown', action: 'next-item' },
{ key: 'ArrowUp', action: 'prev-item' },
{ key: 'Home', action: 'first-item' },
{ key: 'End', action: 'last-item' },
{ key: 'Enter', action: 'activate' },
{ key: ' ', action: 'activate' }
]
},
{
name: 'Content',
kebab: 'content',
kind: 'public',
defaultElement: 'div',
role: 'tabpanel',
optional: false,
states: ['active', 'inactive'],
data: [
{ attr: 'data-state', values: ['active', 'inactive'] },
{ attr: 'data-orientation', values: ['horizontal', 'vertical'] }
],
aria: []
aria: [
{
attr: 'aria-labelledby',
value: v.partRef('trigger'),
condition: { when: 'part-present', part: 'trigger' },
severity: 'recommended'
}
]
}
]
};

@ -1,4 +1,5 @@
import type { Morfo } from '../types';
import { v } from '../types';
export const tagGroupMorfo: Morfo = {
name: 'TagGroup',
@ -10,20 +11,58 @@ export const tagGroupMorfo: Morfo = {
kebab: 'root',
kind: 'public',
defaultElement: 'div',
role: 'grid',
optional: false,
data: [
{ attr: 'data-disabled', severity: 'optional' },
{ attr: 'data-empty', severity: 'optional' },
{ attr: 'data-selection-mode', values: ['none', 'single', 'multiple'] }
],
aria: [
{
attr: 'aria-label',
value: v.translationRef('#?components.tag-group.label|Tags'),
severity: 'recommended'
},
{
attr: 'aria-multiselectable',
value: v.literal('true'),
severity: 'optional',
condition: { when: 'prop-truthy', prop: 'multiple' }
},
{
attr: 'aria-disabled',
value: v.literal('true'),
severity: 'optional',
condition: { when: 'prop-truthy', prop: 'disabled' }
}
],
keyboard: [
{ key: 'ArrowDown', action: 'next-item' },
{ key: 'ArrowUp', action: 'prev-item' },
{ key: 'Home', action: 'first-item' },
{ key: 'End', action: 'last-item' },
{ key: 'Enter', action: 'activate' },
{ key: ' ', action: 'select' },
{ key: 'Delete', action: 'remove-item' },
{ key: 'Backspace', action: 'remove-item' }
]
},
{
name: 'Label',
kebab: 'label',
kind: 'public',
defaultElement: 'div',
optional: true,
data: [],
aria: []
},
{ name: 'Label', kebab: 'label', kind: 'public', defaultElement: 'div', optional: true, data: [], aria: [] },
{
name: 'Item',
kebab: 'item',
kind: 'public',
defaultElement: 'div',
role: 'row',
optional: false,
states: ['selected', 'unselected'],
data: [
@ -31,20 +70,52 @@ export const tagGroupMorfo: Morfo = {
{ attr: 'data-highlighted', severity: 'optional' },
{ attr: 'data-disabled', severity: 'optional' }
],
aria: []
aria: [
{ attr: 'aria-selected', value: v.stateRef('selected'), severity: 'optional' },
{
attr: 'aria-disabled',
value: v.literal('true'),
severity: 'optional',
condition: { when: 'prop-truthy', prop: 'disabled' }
}
]
},
{
name: 'Link',
kebab: 'link',
kind: 'public',
defaultElement: 'a',
role: 'row',
optional: true,
data: [
{ attr: 'data-highlighted', severity: 'optional' },
{ attr: 'data-disabled', severity: 'optional' }
],
aria: []
aria: [
{
attr: 'aria-disabled',
value: v.literal('true'),
severity: 'optional',
condition: { when: 'prop-truthy', prop: 'disabled' }
}
]
},
{ name: 'RemoveButton', kebab: 'remove-button', kind: 'public', defaultElement: 'button', optional: true, data: [], aria: [] }
{
name: 'RemoveButton',
kebab: 'remove-button',
kind: 'public',
defaultElement: 'button',
role: 'button',
optional: true,
data: [],
aria: [
{ attr: 'type', value: v.literal('button') },
{
attr: 'aria-label',
value: v.translationRef('#?components.tag-group.remove|Remove tag'),
severity: 'recommended'
}
]
}
]
};

@ -1,4 +1,5 @@
import type { Morfo } from '../types';
import { v } from '../types';
export const tagsInputMorfo: Morfo = {
name: 'TagsInput',
@ -10,6 +11,7 @@ export const tagsInputMorfo: Morfo = {
kebab: 'root',
kind: 'public',
defaultElement: 'div',
role: 'listbox',
optional: false,
data: [
{ attr: 'data-empty', severity: 'optional' },
@ -17,7 +19,7 @@ export const tagsInputMorfo: Morfo = {
{ attr: 'data-readonly', severity: 'optional' },
{ attr: 'data-invalid', severity: 'optional' }
],
aria: []
aria: [{ attr: 'aria-orientation', value: v.literal('horizontal') }]
},
{
name: 'Control',
@ -38,19 +40,36 @@ export const tagsInputMorfo: Morfo = {
kebab: 'input',
kind: 'public',
defaultElement: 'input',
role: 'combobox',
optional: false,
data: [
{ attr: 'data-disabled', severity: 'optional' },
{ attr: 'data-readonly', severity: 'optional' },
{ attr: 'data-invalid', severity: 'optional' }
],
aria: []
aria: [
{ attr: 'aria-autocomplete', value: v.literal('list') },
{ attr: 'aria-activedescendant', value: v.partRef('item'), severity: 'optional' },
{
attr: 'aria-invalid',
value: v.literal('true'),
severity: 'optional',
condition: { when: 'prop-truthy', prop: 'invalid' }
}
],
keyboard: [
{ key: 'Enter', action: 'add-tag' },
{ key: 'Escape', action: 'clear-input' },
{ key: 'Backspace', action: 'delete-prev' },
{ key: 'ArrowLeft', action: 'focus-prev' }
]
},
{
name: 'Item',
kebab: 'item',
kind: 'public',
defaultElement: 'div',
role: 'option',
optional: false,
states: ['active', 'inactive'],
data: [
@ -58,7 +77,24 @@ export const tagsInputMorfo: Morfo = {
{ attr: 'data-highlighted', severity: 'optional' },
{ attr: 'data-disabled', severity: 'optional' }
],
aria: []
aria: [
{ attr: 'aria-selected', value: v.literal('true') },
{
attr: 'aria-disabled',
value: v.literal('true'),
severity: 'optional',
condition: { when: 'prop-truthy', prop: 'disabled' }
}
],
keyboard: [
{ key: 'ArrowLeft', action: 'prev-item' },
{ key: 'ArrowRight', action: 'next-item' },
{ key: 'Backspace', action: 'delete-item' },
{ key: 'Delete', action: 'delete-item' },
{ key: 'Home', action: 'first-item' },
{ key: 'End', action: 'last-item' },
{ key: 'Escape', action: 'blur' }
]
},
{
name: 'ItemText',
@ -78,10 +114,34 @@ export const tagsInputMorfo: Morfo = {
kebab: 'item-delete-trigger',
kind: 'public',
defaultElement: 'button',
role: 'button',
optional: true,
data: [{ attr: 'data-disabled', severity: 'optional' }],
aria: []
aria: [
{ attr: 'type', value: v.literal('button') },
{
attr: 'aria-label',
value: v.translationRef('#?components.tags-input.remove|Remove tag'),
severity: 'recommended'
}
]
},
{ name: 'ClearTrigger', kebab: 'clear-trigger', kind: 'public', defaultElement: 'button', optional: true, data: [], aria: [] }
{
name: 'ClearTrigger',
kebab: 'clear-trigger',
kind: 'public',
defaultElement: 'button',
role: 'button',
optional: true,
data: [],
aria: [
{ attr: 'type', value: v.literal('button') },
{
attr: 'aria-label',
value: v.translationRef('#?components.tags-input.clear|Clear all tags'),
severity: 'recommended'
}
]
}
]
};

@ -1,4 +1,5 @@
import type { Morfo } from '../types';
import { v } from '../types';
export const timeFieldMorfo: Morfo = {
name: 'TimeField',
@ -24,18 +25,29 @@ export const timeFieldMorfo: Morfo = {
kebab: 'input',
kind: 'public',
defaultElement: 'div',
role: 'group',
optional: false,
data: [
{ attr: 'data-invalid', severity: 'optional' },
{ attr: 'data-disabled', severity: 'optional' }
],
aria: []
aria: [
{ attr: 'aria-label', value: v.propRef('ariaLabel'), severity: 'optional' },
{
attr: 'aria-labelledby',
value: v.partRef('label'),
condition: { when: 'part-present', part: 'label' },
severity: 'recommended'
},
{ attr: 'aria-disabled', value: v.propRef('disabled'), severity: 'optional' }
]
},
{
name: 'Segment',
kebab: 'segment',
kind: 'public',
defaultElement: 'span',
role: 'spinbutton',
optional: false,
data: [
{ attr: 'data-invalid', severity: 'optional' },
@ -44,7 +56,23 @@ export const timeFieldMorfo: Morfo = {
{ attr: 'data-placeholder', severity: 'optional' },
{ attr: 'data-segment' }
],
aria: []
aria: [
{ attr: 'aria-label', value: v.propRef('segmentLabel') },
{ attr: 'aria-valuemin', value: v.propRef('min') },
{ attr: 'aria-valuemax', value: v.propRef('max') },
{ attr: 'aria-valuenow', value: v.propRef('value'), severity: 'optional' },
{ attr: 'aria-valuetext', value: v.propRef('valueText') },
{ attr: 'aria-invalid', value: v.propRef('invalid'), severity: 'optional' },
{ attr: 'aria-disabled', value: v.propRef('disabled'), severity: 'optional' },
{ attr: 'aria-readonly', value: v.propRef('readonly'), severity: 'optional' }
],
keyboard: [
{ key: 'ArrowUp', action: 'increment' },
{ key: 'ArrowDown', action: 'decrement' },
{ key: 'Backspace', action: 'delete' },
{ key: 'ArrowLeft', action: 'prev-segment' },
{ key: 'ArrowRight', action: 'next-segment' }
]
},
{
name: 'Label',

@ -1,26 +1,60 @@
import type { Morfo } from '../types';
import { v } from '../types';
export const timePickerMorfo: Morfo = {
name: 'TimePicker',
kebab: 'time-picker',
scope: ['soma'],
focus: {
initial: 'first-focusable',
trap: true,
return: 'trigger',
restore: true
},
parts: [
{ name: 'Provider', kebab: 'root', kind: 'virtual', defaultElement: 'none', optional: false, data: [], aria: [] },
{
name: 'Provider',
kebab: 'root',
kind: 'virtual',
defaultElement: 'none',
optional: false,
data: [],
aria: []
},
{
name: 'Trigger',
kebab: 'trigger',
kind: 'public',
defaultElement: 'button',
role: 'button',
optional: false,
states: ['open', 'closed'],
data: [{ attr: 'data-state', values: ['open', 'closed'] }],
aria: []
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'),
condition: { when: 'part-present', part: 'content' },
severity: 'recommended'
},
{
attr: 'aria-label',
value: v.translationRef('#?components.time-picker.trigger|Choose time'),
severity: 'recommended'
}
]
},
{
name: 'Content',
kebab: 'content',
kind: 'public',
defaultElement: 'div',
role: 'dialog',
optional: false,
states: ['open', 'closed'],
data: [{ attr: 'data-state', values: ['open', 'closed'] }],

@ -1,4 +1,5 @@
import type { Morfo } from '../types';
import { v } from '../types';
export const timeRangeFieldMorfo: Morfo = {
name: 'TimeRangeField',
@ -10,6 +11,7 @@ export const timeRangeFieldMorfo: Morfo = {
kebab: 'root',
kind: 'public',
defaultElement: 'div',
role: 'group',
optional: false,
data: [
{ attr: 'data-invalid', severity: 'optional' },
@ -17,7 +19,16 @@ export const timeRangeFieldMorfo: Morfo = {
{ attr: 'data-readonly', severity: 'optional' },
{ attr: 'data-required', severity: 'optional' }
],
aria: []
aria: [
{ attr: 'aria-label', value: v.propRef('ariaLabel'), severity: 'optional' },
{
attr: 'aria-labelledby',
value: v.partRef('label'),
condition: { when: 'part-present', part: 'label' },
severity: 'recommended'
},
{ attr: 'aria-disabled', value: v.propRef('disabled'), severity: 'optional' }
]
},
{
name: 'Label',

@ -1,20 +1,47 @@
import type { Morfo } from '../types';
import { v } from '../types';
export const timeRangePickerMorfo: Morfo = {
name: 'TimeRangePicker',
kebab: 'time-range-picker',
scope: ['soma'],
focus: {
initial: 'first-focusable',
trap: true,
return: 'trigger',
restore: true
},
parts: [
{ name: 'Provider', kebab: 'root', kind: 'virtual', defaultElement: 'none', optional: false, data: [], aria: [] },
{
name: 'Provider',
kebab: 'root',
kind: 'virtual',
defaultElement: 'none',
optional: false,
data: [],
aria: []
},
{
name: 'Trigger',
kebab: 'trigger',
kind: 'public',
defaultElement: 'button',
role: 'button',
optional: false,
states: ['open', 'closed'],
data: [{ attr: 'data-state', values: ['open', 'closed'] }],
aria: []
aria: [
{ attr: 'type', value: v.literal('button') },
{ attr: 'aria-haspopup', value: v.literal('dialog') },
{ attr: 'aria-expanded', value: v.stateRef('open') },
{
attr: 'aria-label',
value: v.translationRef('#?components.time-range-picker.trigger|Choose time range'),
severity: 'recommended'
}
]
}
]
};

@ -1,17 +1,42 @@
import type { Morfo } from '../types';
import { v } from '../types';
export const toastMorfo: Morfo = {
name: 'Toast',
kebab: 'toast',
scope: ['soma'],
parts: [
{ name: 'Provider', kebab: 'root', kind: 'virtual', defaultElement: 'none', optional: false, data: [], aria: [] },
{ name: 'Viewport', kebab: 'viewport', kind: 'public', defaultElement: 'div', optional: false, data: [], aria: [] },
{
name: 'Provider',
kebab: 'root',
kind: 'virtual',
defaultElement: 'none',
optional: false,
data: [],
aria: []
},
{
name: 'Viewport',
kebab: 'viewport',
kind: 'public',
defaultElement: 'div',
role: 'region',
optional: false,
data: [],
aria: [
{
attr: 'aria-label',
value: v.translationRef('#?components.toast.viewport|Notifications'),
severity: 'recommended'
}
]
},
{
name: 'Item',
kebab: 'item',
kind: 'public',
defaultElement: 'div',
role: 'status',
optional: false,
states: ['open', 'closed'],
data: [
@ -21,11 +46,53 @@ export const toastMorfo: Morfo = {
{ attr: 'data-type', values: ['default', 'info', 'success', 'warning', 'error', 'loading'] },
{ attr: 'data-swipe', values: ['start', 'move', 'cancel', 'end'] }
],
aria: [{ attr: 'aria-live', value: v.propRef('priority'), severity: 'recommended' }],
keyboard: [{ key: 'Escape', action: 'close' }]
},
{
name: 'Title',
kebab: 'title',
kind: 'public',
defaultElement: 'div',
optional: true,
data: [],
aria: []
},
{
name: 'Description',
kebab: 'description',
kind: 'public',
defaultElement: 'div',
optional: true,
data: [],
aria: []
},
{ name: 'Title', kebab: 'title', kind: 'public', defaultElement: 'div', optional: true, data: [], aria: [] },
{ name: 'Description', kebab: 'description', kind: 'public', defaultElement: 'div', optional: true, data: [], aria: [] },
{ name: 'Action', kebab: 'action', kind: 'public', defaultElement: 'button', optional: true, data: [], aria: [] },
{ name: 'Close', kebab: 'close', kind: 'public', defaultElement: 'button', optional: true, data: [], aria: [] }
{
name: 'Action',
kebab: 'action',
kind: 'public',
defaultElement: 'button',
role: 'button',
optional: true,
data: [],
aria: [{ attr: 'type', value: v.literal('button') }]
},
{
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.translationRef('#?components.toast.close|Close'),
severity: 'recommended'
}
]
}
]
};

@ -1,4 +1,5 @@
import type { Morfo } from '../types';
import { v } from '../types';
export const toggleGroupMorfo: Morfo = {
name: 'ToggleGroup',
@ -10,18 +11,20 @@ export const toggleGroupMorfo: Morfo = {
kebab: 'root',
kind: 'public',
defaultElement: 'div',
role: 'group',
optional: false,
data: [
{ attr: 'data-orientation', values: ['horizontal', 'vertical'] },
{ attr: 'data-disabled', severity: 'optional' }
],
aria: []
aria: [{ attr: 'aria-orientation', value: v.propRef('orientation') }]
},
{
name: 'Item',
kebab: 'item',
kind: 'public',
defaultElement: 'button',
role: 'button',
optional: false,
states: ['on', 'off'],
data: [
@ -29,7 +32,18 @@ export const toggleGroupMorfo: Morfo = {
{ attr: 'data-disabled', severity: 'optional' },
{ attr: 'data-orientation', values: ['horizontal', 'vertical'] }
],
aria: []
aria: [
{ attr: 'type', value: v.literal('button') },
{ attr: 'aria-pressed', value: v.stateRef('on') }
],
keyboard: [
{ key: 'ArrowRight', action: 'next-item' },
{ key: 'ArrowLeft', action: 'prev-item' },
{ key: 'ArrowDown', action: 'next-item' },
{ key: 'ArrowUp', action: 'prev-item' },
{ key: 'Home', action: 'first-item' },
{ key: 'End', action: 'last-item' }
]
}
]
};

@ -1,4 +1,5 @@
import type { Morfo } from '../types';
import { v } from '../types';
export const toggleMorfo: Morfo = {
name: 'Toggle',
@ -20,7 +21,40 @@ export const toggleMorfo: Morfo = {
{ attr: 'data-invalid', severity: 'optional' },
{ attr: 'data-required', severity: 'optional' }
],
aria: []
aria: [
{ attr: 'type', value: v.literal('button') },
{ attr: 'aria-pressed', value: v.stateRef('on') },
{
attr: 'aria-label',
value: v.propRef('ariaLabel'),
severity: 'optional',
condition: { when: 'prop-truthy', prop: 'ariaLabel' }
},
{
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-required',
value: v.literal('true'),
severity: 'optional',
condition: { when: 'prop-truthy', prop: 'required' }
},
{
attr: 'aria-invalid',
value: v.literal('true'),
severity: 'optional',
condition: { when: 'prop-truthy', prop: 'invalid' }
}
]
}
]
};

@ -1,4 +1,5 @@
import type { Morfo } from '../types';
import { v } from '../types';
export const toolbarMorfo: Morfo = {
name: 'Toolbar',
@ -11,21 +12,31 @@ export const toolbarMorfo: Morfo = {
kebab: 'root',
kind: 'public',
defaultElement: 'div',
role: 'toolbar',
optional: false,
data: [{ attr: 'data-orientation', values: ['horizontal', 'vertical'] }],
aria: []
aria: [{ attr: 'aria-orientation', value: v.propRef('orientation') }],
keyboard: [
{ key: 'ArrowRight', action: 'next-item' },
{ key: 'ArrowLeft', action: 'prev-item' },
{ key: 'ArrowDown', action: 'next-item' },
{ key: 'ArrowUp', action: 'prev-item' },
{ key: 'Home', action: 'first-item' },
{ key: 'End', action: 'last-item' }
]
},
{
name: 'Button',
kebab: 'button',
kind: 'public',
defaultElement: 'button',
role: 'button',
optional: true,
data: [
{ attr: 'data-disabled', severity: 'optional' },
{ attr: 'data-orientation', values: ['horizontal', 'vertical'] }
],
aria: []
aria: [{ attr: 'type', value: v.literal('button') }]
},
{
name: 'Link',
@ -41,34 +52,47 @@ export const toolbarMorfo: Morfo = {
kebab: 'group',
kind: 'public',
defaultElement: 'div',
role: 'group',
optional: true,
data: [
{ attr: 'data-orientation', values: ['horizontal', 'vertical'] },
{ attr: 'data-disabled', severity: 'optional' }
],
aria: []
aria: [
{
attr: 'aria-disabled',
value: v.literal('true'),
severity: 'optional',
condition: { when: 'prop-truthy', prop: 'disabled' }
}
]
},
{
name: 'GroupItem',
kebab: 'group-item',
kind: 'public',
defaultElement: 'button',
role: 'button',
optional: true,
states: ['on', 'off'],
data: [
{ attr: 'data-state', values: ['on', 'off'] },
{ attr: 'data-disabled', severity: 'optional' }
],
aria: []
aria: [
{ attr: 'type', value: v.literal('button') },
{ attr: 'aria-pressed', value: v.stateRef('on') }
]
},
{
name: 'Separator',
kebab: 'separator',
kind: 'public',
defaultElement: 'div',
role: 'separator',
optional: true,
data: [{ attr: 'data-orientation', values: ['horizontal', 'vertical'] }],
aria: []
aria: [{ attr: 'aria-orientation', value: v.propRef('orientation') }]
}
]
};

@ -1,4 +1,5 @@
import type { Morfo } from '../types';
import { v } from '../types';
export const tooltipMorfo: Morfo = {
name: 'Tooltip',
@ -6,7 +7,15 @@ export const tooltipMorfo: Morfo = {
scope: ['soma'],
apg: 'https://www.w3.org/WAI/ARIA/apg/patterns/tooltip/',
parts: [
{ name: 'Provider', kebab: 'root', kind: 'virtual', defaultElement: 'none', optional: false, data: [], aria: [] },
{
name: 'Provider',
kebab: 'root',
kind: 'virtual',
defaultElement: 'none',
optional: false,
data: [],
aria: []
},
{
name: 'Trigger',
kebab: 'trigger',
@ -15,18 +24,42 @@ export const tooltipMorfo: Morfo = {
optional: false,
states: ['closed', 'delayed-open', 'instant-open'],
data: [{ attr: 'data-state', values: ['closed', 'delayed-open', 'instant-open'] }],
aria: []
aria: [
{
attr: 'aria-describedby',
value: v.partRef('content'),
condition: { when: 'part-present', part: 'content' },
severity: 'recommended'
}
],
keyboard: [{ key: 'Escape', action: 'close' }]
},
{
name: 'Content',
kebab: 'content',
kind: 'public',
defaultElement: 'div',
role: 'tooltip',
optional: false,
states: ['closed', 'delayed-open', 'instant-open'],
data: [{ attr: 'data-state', values: ['closed', 'delayed-open', 'instant-open'] }],
aria: []
aria: [
{
attr: 'aria-label',
value: v.propRef('ariaLabel'),
severity: 'optional',
condition: { when: 'prop-truthy', prop: 'ariaLabel' }
}
]
},
{ name: 'Arrow', kebab: 'arrow', kind: 'public', defaultElement: 'div', optional: true, data: [], aria: [] }
{
name: 'Arrow',
kebab: 'arrow',
kind: 'public',
defaultElement: 'div',
optional: true,
data: [],
aria: []
}
]
};

@ -1,4 +1,5 @@
import type { Morfo } from '../types';
import { v } from '../types';
export const treeGridMorfo: Morfo = {
name: 'TreeGrid',
@ -11,6 +12,7 @@ export const treeGridMorfo: Morfo = {
kebab: 'root',
kind: 'public',
defaultElement: 'div',
role: 'treegrid',
optional: false,
data: [
{ attr: 'data-disabled', severity: 'optional' },
@ -18,15 +20,71 @@ export const treeGridMorfo: Morfo = {
{ attr: 'data-empty', severity: 'optional' },
{ attr: 'data-selection-mode', values: ['none', 'single', 'multiple'] }
],
aria: [
{
attr: 'aria-label',
value: v.translationRef('#?components.tree-grid.label|Tree grid'),
severity: 'recommended'
},
{
attr: 'aria-multiselectable',
value: v.literal('true'),
severity: 'optional',
condition: { when: 'prop-truthy', prop: 'multiple' }
},
{
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' }
}
],
keyboard: [
{ key: 'ArrowDown', action: 'next-row' },
{ key: 'ArrowUp', action: 'prev-row' },
{ key: 'ArrowRight', action: 'expand' },
{ key: 'ArrowLeft', action: 'collapse' },
{ key: 'Home', action: 'first-row' },
{ key: 'End', action: 'last-row' },
{ key: 'PageUp', action: 'page-up' },
{ key: 'PageDown', action: 'page-down' },
{ key: ' ', action: 'select' },
{ key: 'Enter', action: 'activate' },
{ key: 'Escape', action: 'cancel' }
]
},
{
name: 'Header',
kebab: 'header',
kind: 'public',
defaultElement: 'div',
role: 'row',
optional: true,
data: [],
aria: []
},
{
name: 'ColumnHeader',
kebab: 'column-header',
kind: 'public',
defaultElement: 'div',
role: 'columnheader',
optional: true,
data: [],
aria: []
},
{ name: 'Header', kebab: 'header', kind: 'public', defaultElement: 'div', optional: true, data: [], aria: [] },
{ name: 'ColumnHeader', kebab: 'column-header', kind: 'public', defaultElement: 'div', optional: true, data: [], aria: [] },
{
name: 'Row',
kebab: 'row',
kind: 'public',
defaultElement: 'div',
role: 'row',
optional: false,
states: ['selected', 'unselected'],
data: [
@ -36,19 +94,61 @@ export const treeGridMorfo: Morfo = {
{ attr: 'data-disabled', severity: 'optional' },
{ attr: 'data-level' }
],
aria: [
{ attr: 'aria-level', value: v.propRef('level') },
{
attr: 'aria-expanded',
value: v.propRef('expanded'),
severity: 'optional',
condition: { when: 'prop-truthy', prop: 'hasChildren' }
},
{ attr: 'aria-selected', value: v.stateRef('selected'), severity: 'optional' },
{
attr: 'aria-disabled',
value: v.literal('true'),
severity: 'optional',
condition: { when: 'prop-truthy', prop: 'disabled' }
}
]
},
{
name: 'RowChildren',
kebab: 'row-children',
kind: 'public',
defaultElement: 'div',
role: 'group',
optional: true,
data: [],
aria: []
},
{
name: 'Cell',
kebab: 'cell',
kind: 'public',
defaultElement: 'div',
role: 'gridcell',
optional: false,
data: [],
aria: []
},
{ name: 'RowChildren', kebab: 'row-children', kind: 'public', defaultElement: 'div', optional: true, data: [], aria: [] },
{ name: 'Cell', kebab: 'cell', kind: 'public', defaultElement: 'div', optional: false, data: [], aria: [] },
{
name: 'ExpandTrigger',
kebab: 'expand-trigger',
kind: 'public',
defaultElement: 'button',
role: 'button',
optional: true,
states: ['open', 'closed'],
data: [{ attr: 'data-state', values: ['open', 'closed'] }],
aria: []
aria: [
{ attr: 'type', value: v.literal('button') },
{
attr: 'aria-label',
value: v.translationRef('#?components.tree-grid.toggle|Toggle row'),
severity: 'recommended'
},
{ attr: 'aria-hidden', value: v.literal('true') }
]
}
]
};

@ -1,4 +1,5 @@
import type { Morfo } from '../types';
import { v } from '../types';
export const treeViewMorfo: Morfo = {
name: 'TreeView',
@ -6,12 +7,44 @@ export const treeViewMorfo: Morfo = {
scope: ['soma'],
apg: 'https://www.w3.org/WAI/ARIA/apg/patterns/treeview/',
parts: [
{ name: 'Provider', kebab: 'root', kind: 'public', defaultElement: 'ul', optional: false, data: [], aria: [] },
{
name: 'Provider',
kebab: 'root',
kind: 'public',
defaultElement: 'ul',
role: 'tree',
optional: false,
data: [],
aria: [
{
attr: 'aria-label',
value: v.translationRef('#?components.tree-view.label|Tree'),
severity: 'recommended'
},
{
attr: 'aria-multiselectable',
value: v.literal('true'),
severity: 'optional',
condition: { when: 'prop-truthy', prop: 'multiple' }
}
],
keyboard: [
{ key: 'ArrowDown', action: 'next-item' },
{ key: 'ArrowUp', action: 'prev-item' },
{ key: 'ArrowRight', action: 'expand' },
{ key: 'ArrowLeft', action: 'collapse' },
{ key: 'Home', action: 'first-item' },
{ key: 'End', action: 'last-item' },
{ key: 'Enter', action: 'activate' },
{ key: ' ', action: 'select' }
]
},
{
name: 'Branch',
kebab: 'branch',
kind: 'public',
defaultElement: 'li',
role: 'treeitem',
optional: false,
states: ['open', 'closed'],
data: [
@ -20,7 +53,22 @@ export const treeViewMorfo: Morfo = {
{ attr: 'data-disabled', severity: 'optional' },
{ attr: 'data-depth' }
],
aria: []
aria: [
{ attr: 'aria-expanded', value: v.stateRef('open') },
{
attr: 'aria-selected',
value: v.literal('true'),
severity: 'optional',
condition: { when: 'prop-truthy', prop: 'selected' }
},
{ attr: 'aria-level', value: v.propRef('depth') },
{
attr: 'aria-disabled',
value: v.literal('true'),
severity: 'optional',
condition: { when: 'prop-truthy', prop: 'disabled' }
}
]
},
{
name: 'BranchControl',
@ -41,6 +89,7 @@ export const treeViewMorfo: Morfo = {
kebab: 'branch-content',
kind: 'public',
defaultElement: 'ul',
role: 'group',
optional: false,
states: ['open', 'closed'],
data: [{ attr: 'data-state', values: ['open', 'closed'] }],
@ -54,7 +103,7 @@ export const treeViewMorfo: Morfo = {
optional: true,
states: ['open', 'closed'],
data: [{ attr: 'data-state', values: ['open', 'closed'] }],
aria: []
aria: [{ attr: 'aria-hidden', value: v.literal('true') }]
},
{
name: 'BranchIndentGuide',
@ -63,21 +112,44 @@ export const treeViewMorfo: Morfo = {
defaultElement: 'div',
optional: true,
data: [],
aria: []
aria: [{ attr: 'aria-hidden', value: v.literal('true') }]
},
{
name: 'Item',
kebab: 'item',
kind: 'public',
defaultElement: 'li',
role: 'treeitem',
optional: false,
data: [
{ attr: 'data-selected', severity: 'optional' },
{ attr: 'data-disabled', severity: 'optional' },
{ attr: 'data-depth' }
],
aria: []
aria: [
{
attr: 'aria-selected',
value: v.literal('true'),
severity: 'optional',
condition: { when: 'prop-truthy', prop: 'selected' }
},
{ attr: 'aria-level', value: v.propRef('depth') },
{
attr: 'aria-disabled',
value: v.literal('true'),
severity: 'optional',
condition: { when: 'prop-truthy', prop: 'disabled' }
}
]
},
{ name: 'Label', kebab: 'label', kind: 'public', defaultElement: 'div', optional: true, data: [], aria: [] }
{
name: 'Label',
kebab: 'label',
kind: 'public',
defaultElement: 'div',
optional: true,
data: [],
aria: []
}
]
};

Loading…
Cancel
Save

Powered by TurnKey Linux.