refactor(uix): share FloatPanel pointer gesture listeners

astra
dev 2 weeks ago
parent 45d87911b1
commit 25ff523d08

@ -360,6 +360,30 @@ pruebas**, incluido el check, build y smoke 27/27 de `apps/base`; el smoke compr
arranque y consola, pero no constituye una comparación visual específica de
FloatPanel.
### Segunda pasada de Soma en FloatPanel: listeners de gestos (2026-09-24)
Se reutiliza la matriz de cuatro referentes de la revisión anterior de
FloatPanel: no hay ninguna decisión nueva sobre anatomía, geometría, accesibilidad
o API. El alcance autorizado sigue siendo una deduplicación interna y neutral,
sin tocar `web/`. `startDrag` y `startResize` repetían el alta de
`pointermove`, `pointerup` y `pointercancel`, y la retirada de esos listeners
en el mismo orden. Un método privado concentra sólo ese ciclo; ambos handlers
conservan sus condiciones de entrada, coordenadas, estados, señales y commits.
Una prueba parametrizada pasó **antes y después** del refactor para Drag y Resize,
tanto con `pointerup` como con `pointercancel`: verifica que la geometría no se
confirma hasta la liberación y que los tres listeners se retiran. Los dos archivos
dirigidos pasan ahora **16/16 pruebas** (12 previas y 4 nuevas); la auditoría de
FloatPanel mantiene **PASS**. El provider pasa de **1.347 a 1.344 líneas no
vacías**, tres menos; el diff físico de producción es +22/−24 líneas.
`check:gate` acepta los 89 errores ya registrados y `docs:check` termina con
cero errores y avisos. Lint, contratos, build y smoke 27/27 de `apps/base`
pasaron. La primera suite completa falló en las dos pruebas conocidas de
`boot-check` por el temporal de Windows fuera del workspace; con `TEMP` y `TMP`
dentro del workspace pasó **480/480 archivos y 5.508/5.508 pruebas**. La
verificación de gestos es automatizada; no se hizo una comparación visual
específica de FloatPanel en esta pasada.
### Revisión de Chronos (2026-09-22)
El provider contiene 1.723 líneas físicas no vacías, pero su volumen no procede

@ -221,6 +221,67 @@ describe('FloatPanelProvider', () => {
dom.dispose();
});
it.each([
{ gesture: 'drag', finish: 'pointerup' },
{ gesture: 'drag', finish: 'pointercancel' },
{ gesture: 'resize', finish: 'pointerup' },
{ gesture: 'resize', finish: 'pointercancel' }
] as const)(
'commits $gesture on $finish and removes its pointer listeners',
({ gesture, finish }) => {
const { dom } = installSomaHarness();
const opts = createOpts();
opts.position.current = { x: 50, y: 60 };
opts.size.current = { width: 200, height: 150 };
const { result: provider, cleanup } = withEffectRoot(() => FloatPanelProvider.create(opts));
const content = document.createElement('div');
document.body.append(content);
provider.setContentRef(content);
const add = vi.spyOn(document, 'addEventListener');
const remove = vi.spyOn(document, 'removeEventListener');
content.addEventListener('pointerdown', (event) => {
if (gesture === 'drag') provider.startDrag(event);
else provider.startResize(event, 'se');
});
content.dispatchEvent(
new PointerEvent('pointerdown', {
bubbles: true,
button: 0,
pointerId: 7,
clientX: 100,
clientY: 100
})
);
expect(gesture === 'drag' ? provider.dragging : provider.resizing).toBe(true);
document.dispatchEvent(
new PointerEvent('pointermove', { pointerId: 7, clientX: 125, clientY: 125 })
);
// Geometry is written imperatively during the gesture and committed on release.
expect(opts.position.current).toEqual({ x: 50, y: 60 });
expect(opts.size.current).toEqual({ width: 200, height: 150 });
document.dispatchEvent(new PointerEvent(finish, { pointerId: 7 }));
expect(provider.dragging).toBe(false);
expect(provider.resizing).toBe(false);
expect(opts.position.current).toEqual(
gesture === 'drag' ? { x: 75, y: 85 } : { x: 50, y: 60 }
);
expect(opts.size.current).toEqual(
gesture === 'resize' ? { width: 225, height: 175 } : { width: 200, height: 150 }
);
const pointerTypes = ['pointermove', 'pointerup', 'pointercancel'];
expect(
add.mock.calls.map(([type]) => type).filter((type) => pointerTypes.includes(type))
).toEqual(pointerTypes);
expect(
remove.mock.calls.map(([type]) => type).filter((type) => pointerTypes.includes(type))
).toEqual(pointerTypes);
cleanup();
dom.dispose();
}
);
it('keyboard grab-mode moves, reverts on cancel, and guards by stage', () => {
const { dom } = installSomaHarness();
const opts = createOpts();

@ -710,6 +710,20 @@ export class FloatPanelProvider {
return Math.round(value * dpr) / dpr;
}
private listenPointerGesture(
element: HTMLElement | null,
onMove: (event: PointerEvent) => void,
onEnd: (event: PointerEvent) => void
): () => void {
const doc = this.soma.dom.getDocument(element);
const remove = [
this.soma.dom.listen(doc, 'pointermove', (event) => onMove(event as PointerEvent)),
this.soma.dom.listen(doc, 'pointerup', (event) => onEnd(event as PointerEvent)),
this.soma.dom.listen(doc, 'pointercancel', (event) => onEnd(event as PointerEvent))
];
return () => remove.forEach((dispose) => dispose());
}
// ── Drag (move) ─────────────────────────────────────────────────────────
private dragCleanup: (() => void) | null = null;
@ -756,19 +770,11 @@ export class FloatPanelProvider {
// setProperty delegate, so the same-task timing is preserved.)
if (this.contentRef.current)
this.soma.dom.writeProperty(this.contentRef.current, 'will-change', 'translate');
const doc = this.soma.dom.getDocument(this.dragEl);
const move = this.soma.dom.listen(doc, 'pointermove', (ev) =>
this.onDragMove(ev as PointerEvent)
);
const up = this.soma.dom.listen(doc, 'pointerup', (ev) => this.endDrag(ev as PointerEvent));
const cancel = this.soma.dom.listen(doc, 'pointercancel', (ev) =>
this.endDrag(ev as PointerEvent)
this.dragCleanup = this.listenPointerGesture(
this.dragEl,
(event) => this.onDragMove(event),
(event) => this.endDrag(event)
);
this.dragCleanup = () => {
move();
up();
cancel();
};
}
private onDragMove(e: PointerEvent): void {
@ -890,19 +896,11 @@ export class FloatPanelProvider {
this.resizing = true;
if (this.contentRef.current)
this.soma.dom.writeProperty(this.contentRef.current, 'will-change', 'translate');
const doc = this.soma.dom.getDocument(this.resizeEl);
const move = this.soma.dom.listen(doc, 'pointermove', (ev) =>
this.onResizeMove(ev as PointerEvent)
);
const up = this.soma.dom.listen(doc, 'pointerup', (ev) => this.endResize(ev as PointerEvent));
const cancel = this.soma.dom.listen(doc, 'pointercancel', (ev) =>
this.endResize(ev as PointerEvent)
this.resizeCleanup = this.listenPointerGesture(
this.resizeEl,
(event) => this.onResizeMove(event),
(event) => this.endResize(event)
);
this.resizeCleanup = () => {
move();
up();
cancel();
};
}
private onResizeMove(e: PointerEvent): void {

Loading…
Cancel
Save

Powered by TurnKey Linux.