You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
121 lines
6.7 KiB
121 lines
6.7 KiB
# PLAN — eje focus-first (firmado 2026-08-26)
|
|
|
|
**Firma del autor**: «lo mejor para la arquitectura y diseño del framework
|
|
independientemente de lo que cueste» + «ejecuta el eje focus-first completo».
|
|
La amputación de `Morfo.focus` queda descartada; la dirección es la inversa:
|
|
**completar el contrato y darle ejecutor y censo**.
|
|
|
|
**Origen**: informe de auditoría (hallazgo verificado nº8) + contraste del
|
|
corpus (eidos.md:472 falso; regla 2-de-3 a cero) + la inversión del autor:
|
|
«el error quizás sean los componentes que deberían declararlo». Censo que
|
|
funda el eje: **~21+ componentes con comportamiento de foco** (7 con
|
|
`FocusScope`: dialog, drawer, popover, context-menu, dropdown-menu,
|
|
float-panel, menubar · **14 de familia roving** con tab-stop único
|
|
reimplementado a mano: toolbar, tabs, listbox, radio-group, stepper,
|
|
tag-group, tags-input, toggle-group, carousel, drag-drop, tree-view,
|
|
tree-grid, grid-list, chronos) contra 9 declaraciones históricas — y la
|
|
forma vieja `{initial, trap, return, restore}` **no podía expresar roving**.
|
|
|
|
## La tesis del eje
|
|
|
|
1. **La declaración es el DEFAULT de especie; la prop por-instancia manda**
|
|
(patrón `dir`). `<Dialog modal={false}>` sigue decidiendo en el consumidor.
|
|
2. **Dos familias, una unión discriminada** — la forma vieja era un fragmento
|
|
para overlays; la nueva nombra el mecanismo:
|
|
```ts
|
|
type MorfoFocus =
|
|
| {
|
|
kind: 'trap';
|
|
trap: boolean; // default de especie
|
|
initial?: 'first-focusable' | 'trigger' | { partRef: string };
|
|
return?: 'trigger' | 'previous' | { partRef: string };
|
|
restore?: boolean;
|
|
}
|
|
| {
|
|
kind: 'roving';
|
|
// ENSANCHADO en F2: toolbar rueda sobre VARIAS partes
|
|
// (button, link, group-item) — un partRef singular mentiría.
|
|
parts: readonly string[];
|
|
orientation: 'horizontal' | 'vertical' | 'both' | 'grid';
|
|
loop?: boolean;
|
|
executor?: { custom: string };
|
|
}; // excepción FIRMADA (censo)
|
|
```
|
|
Validación en schema: `partRef` cruzado contra las partes; `custom` exige
|
|
razón no vacía; `grid` sin `executor.custom` es inválido mientras el
|
|
primitivo sea 1D (la mentira estructural se hace imposible, no prohibida).
|
|
3. **Dos ejecutores, uno por familia**:
|
|
- `trap` → `FocusScope` gana una entrada `policy` (el `compiled.focus`) y
|
|
la usa como fallback real: `trap = trapFocus ?? modal ?? policy.trap`.
|
|
- `roving` → **renace `RovingFocusGroup`** (`$adom`, hoy muerto): se
|
|
corrige su defecto medido (tab-stop por id → por elemento) y pasa a ser
|
|
EL ejecutor del patrón «exactamente un tab-stop»; las reimplementaciones
|
|
a mano colapsan por olas (abajo).
|
|
4. **El censo hace imposible mentir** (patrón `direction-census.test.ts`,
|
|
`focus-census.test.ts`):
|
|
- morfo `trap` ⇒ su provider usa `FocusScope` Y el default de especie del
|
|
wrapper (`modal`/`trapFocus`) coincide con `declaration.trap` — la clase
|
|
Popover (declaraba true, embarcaba false) muere aquí;
|
|
- morfo `roving` ⇒ el provider consume `RovingFocusGroup` O lleva
|
|
`executor.custom` firmado;
|
|
- **la inversa del autor**: componente que usa FocusScope/roving SIN
|
|
declaración ⇒ FALLO. Excepciones, solo firmadas en el propio censo.
|
|
5. **Segundo consumidor = docs-site**: la pestaña A11y de cada componente
|
|
renderiza la política de foco desde la declaración (como ya hace con
|
|
`keyboard[]`) → la regla 2-de-3 se cumple con dos consumidores REALES
|
|
(ejecutores + docs), no por gracia.
|
|
|
|
## Fases (cada una = commit(s) + verificación)
|
|
|
|
- **F0 · árbol limpio** — vicen-42 revierte su amputación sin commitear
|
|
(types/schema/compile + 9 morfos). Nadie más toca su delta.
|
|
→ verificar: `git status` limpio en ese frente; `MorfoFocus` presente.
|
|
- **F1 · el contrato** — la unión discriminada en `types.ts`, validación en
|
|
`schema.ts` (partRef cruzado, custom con razón, grid⇒custom), paso por
|
|
`compile.ts` (`CompiledMorfo.focus`), y `SomaRuntime` expone
|
|
`runtime.focus`.
|
|
→ verificar: `schema.test` + `compile.test` con casos nuevos (inválidos
|
|
incluidos); `npm run check` src/ a 0.
|
|
- **F2 · las declaraciones (~21+)** — los 9 históricos se reescriben a
|
|
`kind: 'trap'` **con la verdad embarcada** (popover: `trap: false`); los 14
|
|
roving declaran `kind: 'roving'` con su orientación real; barrido del resto
|
|
del catálogo por si hay foco sin censar.
|
|
→ verificar: sweep de `validateMorfo` (161+) verde; el censo F5 en rojo
|
|
CONTROLADO (aún sin ejecutores) documenta el estado intermedio.
|
|
- **F3 · ejecutor trap** — `FocusScope.use` acepta `policy`; los 7 providers
|
|
pasan `runtime.focus`; ningún default de wrapper contradice la declaración.
|
|
→ verificar: suites de los 7 overlays; test nuevo: policy como fallback
|
|
(sin props → manda la declaración; con `modal` → manda la prop).
|
|
- **F4 · ejecutor roving** — (a) `RovingFocusGroup` corregido (tab-stop por
|
|
elemento, `''` inválido, membresía viva) + su suite; (b) **ola 1** de
|
|
migración (los 1D limpios: toolbar, tabs, radio-group, toggle-group,
|
|
tag-group, menubar); (c) **ola 2** (listbox, stepper, carousel,
|
|
tags-input — cada uno con su matiz: typeahead, linear-gate…);
|
|
(d) los 2D/complejos (tree-view, tree-grid, grid-list, chronos,
|
|
drag-drop) declaran `executor: { custom: <razón> }` — firmados, no
|
|
migrados a la fuerza a un primitivo 1D.
|
|
→ verificar por ola: suite del componente + `perm:check` de sus rutas +
|
|
interacción de flechas/Home/End en navegador.
|
|
- **F5 · el censo** — `focus-census.test.ts` con las tres direcciones y las
|
|
excepciones firmadas en el fichero.
|
|
→ verificar: censo verde; plantar una mentira (declarar trap:true en
|
|
popover) y ver el censo en ROJO — un guard que no sabe fallar no vale.
|
|
- **F6 · doctrina y docs-site** — morfo.md (fila de `focus` = ejecutada POR
|
|
FIN, con sus dos ejecutores), eidos.md:472 corregida, soma-architecture,
|
|
glossary; la pestaña A11y del docs-site renderiza la política.
|
|
→ verificar: `docs:check` 0; página de un trap y un roving en navegador.
|
|
- **F7 · cierre** — `npm run gate` entero + `morfo:check` + actualización de
|
|
CONTINUE-audit-p0.md y memoria.
|
|
|
|
## Leyes que este eje debe respetar
|
|
|
|
- Árbol compartido: add por lista explícita, verificar+commitear en la misma
|
|
invocación; los deltas de otros ejes no se tocan ni se stagean.
|
|
- La declaración NUNCA contradice lo embarcado: donde difieran, la verdad
|
|
embarcada manda y la declaración se corrige (Popover es el precedente).
|
|
- Un componente 2D no se fuerza al primitivo 1D: excepción firmada > mentira
|
|
estructural.
|
|
- `runed`/`tabbable` port: `continue-runed-tabbable-port.md` existe en
|
|
process/ — leerlo antes de F4a por si el port pactó decisiones sobre el
|
|
tab-stop.
|