From 2fcade063b07b622a2aa30fdea39b37348786855 Mon Sep 17 00:00:00 2001 From: dev Date: Wed, 20 May 2026 22:10:28 +0200 Subject: [PATCH] =?UTF-8?q?tooltip:=20expand=20morfo=20+=20recipe=20states?= =?UTF-8?q?=20=E2=86=92=20PASS=20audit?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes the second BROKEN component from the audit (after date-range-picker). Five errors collapse to zero: - Morfo declares the three `emerge` events the recipe animations were already keying off (`open`, `close`, `close-dismiss`) plus a `texts.label` slot. `data-disabled` is now declared on `Trigger` (with optional severity) so the recipe rule has a real contract to land on. `aria-disabled` and `texts.label` round out the surface; the new `tooltip.label` catalog entry ships under `src/uix/langs/components/tooltip.ts`. - Recipe CSS: explicit `[data-tooltip-trigger][data-disabled]` and `[data-tooltip-trigger]:focus-visible` rules. The disabled rule suppresses pointer events; the focus rule provides a defensive outline so a focusable trigger shell is at least keyboard-visible. - README: convert the old `## Pendiente deliberado` section into a proper `## Gaps` table with explicit dispositions (implementar/diferir/descartar), and update `## Eventos Sema` to reflect that tooltip now declares its three emerge events instead of claiming zero. Comparativa was already complete; the regex fix from the previous commit picked it up. Audit: tooltip BROKEN → PASS. Project-wide goes from 1/64/2 to 3/64/0 (PASS / NEEDS-WORK / BROKEN). No more BROKEN components. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/uix/eidos/components/tooltip/README.md | 26 ++++---- src/uix/eidos/components/tooltip/tooltip.css | 18 ++++++ src/uix/langs/components/index.ts | 2 + src/uix/langs/components/tooltip.ts | 15 +++++ src/uix/morfo/components/tooltip.ts | 65 +++++++++++++++++++- 5 files changed, 112 insertions(+), 14 deletions(-) create mode 100644 src/uix/langs/components/tooltip.ts diff --git a/src/uix/eidos/components/tooltip/README.md b/src/uix/eidos/components/tooltip/README.md index 27d24b05d..95baa3802 100644 --- a/src/uix/eidos/components/tooltip/README.md +++ b/src/uix/eidos/components/tooltip/README.md @@ -42,18 +42,20 @@ Fuentes externas consultadas: - Soma cierra al activar/clicar el trigger por defecto, alineado con Radix. Si se necesita mantenerlo abierto, usar `disableCloseOnTriggerClick`. -## Pendiente deliberado +## Eventos Sema -- `ignoreNonKeyboardFocus` de Bits queda fuera hasta que haya un caso real en - accesibilidad/focus management. No debe vivir en Eidos. -- `customAnchor` para Tooltip queda fuera por ahora; Popover ya cubre el caso - de panel flotante anclado a otro elemento. Si Tooltip lo necesita, debe - anadirse primero a Soma. +El morfo declara tres eventos `emerge` minimos para que Sema pueda emitir +percepcion en hover/focus reveal: `open`, `close` y `close-dismiss` (Escape). +Ninguno carga intent — la aparicion de un tooltip no tiene peso evaluativo +propio. Si una experiencia necesita reforzar percepcion al mostrar ayuda +contextual con un tinte concreto, esa politica debe vivir en el componente +que dispara el tooltip, no en Tooltip por defecto. -## Eventos Sema +## Gaps -Tooltip declara 0 eventos semanticos de forma deliberada. Su apertura/cierre es -disclosure asistivo de hover/focus, no commit, seleccion ni feedback evaluativo. -Si una experiencia necesita percepcion al mostrar ayuda contextual, esa politica -debe declararse en el componente que dispara la ayuda, no en Tooltip por -defecto. +| ID | Disposicion | Detalle | +| --- | --- | --- | +| `ignoreNonKeyboardFocus` (Bits UI) | **diferir** | Sin caso real en accesibilidad/focus management. No debe vivir en Eidos hasta que aparezca el patron concreto. | +| `customAnchor` (Bits UI) | **diferir** | Popover ya cubre panel flotante anclado a otro elemento. Si Tooltip lo necesita, anadir primero a Soma. | +| `data-disabled` aplicado a `Trigger` | **implementar** | Morfo declara `data-disabled` opcional; soma debe propagar la prop del wrapped element. La receta ya estiliza el estado disabled. | +| Tinte sema configurable por `intent` prop | **descartar** | Reveal asistivo no carga intent; forzar uno duplica responsabilidades con el componente que dispara el tooltip. | diff --git a/src/uix/eidos/components/tooltip/tooltip.css b/src/uix/eidos/components/tooltip/tooltip.css index c57c22675..07ac29ae2 100644 --- a/src/uix/eidos/components/tooltip/tooltip.css +++ b/src/uix/eidos/components/tooltip/tooltip.css @@ -156,6 +156,24 @@ to { opacity: 0; transform: translateX(2px); } } +/* ── Trigger states ───────────────────────────────────────────────────────── + * Trigger is the wrapper around any focusable element (button, link, …). + * The audit (R-1.2 / R-1.5) requires the recipe to acknowledge `data-disabled` + * and `:focus-visible`. The wrapped element keeps its own styling; the rules + * here suppress the tooltip behavior cleanly when the trigger is disabled. */ + +[data-tooltip-trigger][data-disabled] { + pointer-events: none; + cursor: not-allowed; +} + +[data-tooltip-trigger]:focus-visible { + /* Defer the focus ring to the wrapped element — but ensure the outline + is at least visible if the trigger is rendered as a focusable shell. */ + outline: var(--focus-ring-width) solid var(--focus-ring-color); + outline-offset: var(--focus-ring-offset); +} + /* ── Reduced motion ───────────────────────────────────────────────────────── * Honour the user's OS-level preference: tooltips appear instantly with * no slide-in / slide-out, just a binary visibility toggle. The exit diff --git a/src/uix/langs/components/index.ts b/src/uix/langs/components/index.ts index a8afc3bfa..52a9bb6a2 100644 --- a/src/uix/langs/components/index.ts +++ b/src/uix/langs/components/index.ts @@ -40,6 +40,7 @@ import { timeFieldLangs } from './time-field'; import { timePickerLangs } from './time-picker'; import { timeRangeFieldLangs } from './time-range-field'; import { timeRangePickerLangs } from './time-range-picker'; +import { tooltipLangs } from './tooltip'; import { treeGridLangs } from './tree-grid'; import { treeViewLangs } from './tree-view'; @@ -89,6 +90,7 @@ export const componentLangs = { 'time-picker': timePickerLangs, 'time-range-field': timeRangeFieldLangs, 'time-range-picker': timeRangePickerLangs, + tooltip: tooltipLangs, 'tree-grid': treeGridLangs, 'tree-view': treeViewLangs } satisfies LangNode; diff --git a/src/uix/langs/components/tooltip.ts b/src/uix/langs/components/tooltip.ts new file mode 100644 index 000000000..3d80a6921 --- /dev/null +++ b/src/uix/langs/components/tooltip.ts @@ -0,0 +1,15 @@ +import type { LangNode } from '$libs/langs'; + +/** + * Default strings for the tooltip component. Merged under + * `components.tooltip.*` by `ActiveUix` (via `componentLangs` barrel). + * + * Morfo references entries by absolute idlangref: + * `'#?components.tooltip.{key}|fallback'`. + */ +export const tooltipLangs = { + label: { + es: 'Información complementaria', + en: 'Tooltip' + } +} satisfies LangNode; diff --git a/src/uix/morfo/components/tooltip.ts b/src/uix/morfo/components/tooltip.ts index 51385e178..1bc605f1e 100644 --- a/src/uix/morfo/components/tooltip.ts +++ b/src/uix/morfo/components/tooltip.ts @@ -1,11 +1,70 @@ import type { Morfo } from '../types'; import { v } from '../types'; +/** + * Tooltip — non-modal hover/focus reveal. + * + * Two open states: `delayed-open` (hover, animated) and `instant-open` + * (keyboard focus, no entrance). The recipe CSS keys its entrance/exit + * animations off these values. + */ export const tooltipMorfo = { name: 'Tooltip', kebab: 'tooltip', scope: ['soma', 'eidos'], apg: 'https://www.w3.org/WAI/ARIA/apg/patterns/tooltip/', + texts: { + label: '#?components.tooltip.label|Tooltip' + }, + + events: [ + { + // Pointer- or focus-initiated reveal. + name: 'open', + semantic: { + family: 'emerge', + verb: 'open', + target: v.partRef('content'), + sequence: 'pre' + }, + commits: { + part: v.partRef('content'), + attr: 'data-state', + value: 'delayed-open' + } + }, + { + // Pointer-leave or blur close. + name: 'close', + semantic: { + family: 'emerge', + verb: 'close', + target: v.partRef('content'), + sequence: 'pre' + }, + commits: { + part: v.partRef('content'), + attr: 'data-state', + value: 'closed' + } + }, + { + // Escape key dismiss. + name: 'close-dismiss', + semantic: { + family: 'emerge', + verb: 'dismiss', + target: v.partRef('content'), + sequence: 'pre' + }, + commits: { + part: v.partRef('content'), + attr: 'data-state', + value: 'closed' + } + } + ], + parts: [ { name: 'Provider', @@ -30,7 +89,8 @@ export const tooltipMorfo = { attr: 'data-state', values: ['closed', 'delayed-open', 'instant-open'], value: v.stateRef('instant-open') - } + }, + { attr: 'data-disabled', value: v.propRef('disabled'), severity: 'optional' } ], aria: [ { @@ -38,7 +98,8 @@ export const tooltipMorfo = { value: v.partRef('content'), condition: { when: 'part-present', part: 'content' }, severity: 'recommended' - } + }, + { attr: 'aria-disabled', value: v.propRef('disabled'), severity: 'optional' } ], keyboard: [{ key: 'Escape', action: 'close' }] },