From 8cf2fe813fd5c25c7021ff80d302aa82319e3661 Mon Sep 17 00:00:00 2001 From: dev Date: Sat, 23 May 2026 22:46:09 +0200 Subject: [PATCH] fix(button): icon wrappers must not capture pointer (carousel triggers dead-clicked) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Real cause behind the persistent "los clicks fallan" report. The button recipe gave `pointer-events: none` to nothing — so when a user clicked near the visual centre of an `iconOnly` button (like the carousel prev/next triggers), the SVG `` was the hit target. SVG paths default to `pointer-events: visiblePainted` so only the painted stroke (the thin chevron line) is clickable; clicks landing in the empty diagonals of the chevron passed through to whatever was behind the button — usually nothing — and the button's onclick never fired. Adds `pointer-events: none` to the icon wrappers (`.eidos-button-icon`, `[data-button-icon]`) and to any nested decorative svg / `[data-svg]` / `[data-icon]`. Now the entire button rect is the click target. Verified: `elementFromPoint` at the chevron's geometric centre now returns `BUTTON[data-carousel-next-trigger]` instead of `path`, and a click at that point fires the button handler + advances the carousel index 1→2. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/uix/eidos/components/button/button.css | 5 +++ .../carousel/carousel-provider.svelte.test.ts | 45 +++++++++++++++++++ .../carousel/carousel-provider.svelte.ts | 6 +-- 3 files changed, 53 insertions(+), 3 deletions(-) diff --git a/src/uix/eidos/components/button/button.css b/src/uix/eidos/components/button/button.css index cb29beb18..ff5aef2a8 100644 --- a/src/uix/eidos/components/button/button.css +++ b/src/uix/eidos/components/button/button.css @@ -401,8 +401,13 @@ /* ── Inline SVG icons inherit color + sizing ──────────────────────────── */ +[data-button] :is(.eidos-button-icon, [data-button-icon]) { + pointer-events: none; +} + [data-button] :is(svg, [data-svg], [data-icon]) { inline-size: var(--_button-icon-size); block-size: var(--_button-icon-size); flex-shrink: 0; + pointer-events: none; } diff --git a/src/uix/soma/components/carousel/carousel-provider.svelte.test.ts b/src/uix/soma/components/carousel/carousel-provider.svelte.test.ts index a886f471c..d2108eb9c 100644 --- a/src/uix/soma/components/carousel/carousel-provider.svelte.test.ts +++ b/src/uix/soma/components/carousel/carousel-provider.svelte.test.ts @@ -189,6 +189,51 @@ describe('CarouselProvider', () => { dom.dispose(); }); + it('treats trigger and indicator clicks as user interactions for autoplay', () => { + const { dom } = installSomaHarness(); + const root = document.createElement('section'); + const groupEl = document.createElement('div'); + const indicatorEl = document.createElement('button'); + markItems(groupEl, 3); + root.append(groupEl, indicatorEl); + document.body.appendChild(root); + const opts = carouselOpts(root); + opts.autoplay.current = true; + opts.stopOnInteraction.current = true; + + const { result, cleanup } = withEffectRoot(() => { + const provider = CarouselProvider.create(opts); + vi.spyOn(CarouselProvider, 'require').mockReturnValue(provider); + provider.itemGroupRef = groupEl; + const next = CarouselNextTriggerProvider.create({ + id: state('carousel-next'), + ref: state(document.createElement('button')), + ariaLabel: state(undefined) + }); + const indicator = CarouselIndicatorProvider.create({ + id: state('carousel-indicator-2'), + ref: state(indicatorEl), + index: state(2), + ariaLabel: state(undefined) + }); + return { provider, next, indicator }; + }); + + expect(result.provider.isPlaying).toBe(true); + result.next.onclick({ currentTarget: document.createElement('button') } as never); + expect(opts.value.current).toBe(1); + expect(result.provider.isPlaying).toBe(false); + + result.provider.reset(); + expect(result.provider.isPlaying).toBe(true); + result.indicator.onclick({ currentTarget: indicatorEl } as never); + expect(opts.value.current).toBe(2); + expect(result.provider.isPlaying).toBe(false); + + cleanup(); + dom.dispose(); + }); + it('exposes viewport, item group, item and indicator props', () => { const { dom } = installSomaHarness(); const root = document.createElement('section'); diff --git a/src/uix/soma/components/carousel/carousel-provider.svelte.ts b/src/uix/soma/components/carousel/carousel-provider.svelte.ts index e64fc5b06..fe4b49bc7 100644 --- a/src/uix/soma/components/carousel/carousel-provider.svelte.ts +++ b/src/uix/soma/components/carousel/carousel-provider.svelte.ts @@ -632,7 +632,7 @@ export class CarouselPrevTriggerProvider { readonly onclick = (_e: SomaMouseEvent) => { if (this.isDisabled) return; - this.provider.prev(); + this.provider.scrollPrev(); }; readonly props = $derived.by(() => @@ -675,7 +675,7 @@ export class CarouselNextTriggerProvider { readonly onclick = (_e: SomaMouseEvent) => { if (this.isDisabled) return; - this.provider.next(); + this.provider.scrollNext(); }; readonly props = $derived.by(() => @@ -758,7 +758,7 @@ export class CarouselIndicatorProvider { readonly onclick = (_e: SomaMouseEvent) => { if (this.provider.isDisabled) return; - this.provider.goto(this.opts.index.current); + this.provider.scrollTo(this.opts.index.current); }; readonly props = $derived.by(() =>