From 56e985c40f50171bdf0eb99f9b5e2a1db964ac3c Mon Sep 17 00:00:00 2001 From: Andreas Reinhold / reini Date: Mon, 14 Sep 2026 13:36:26 +0200 Subject: [PATCH] Leave a carousel item at the padded focal size where it is Plan step 32, found by the Browser suite. Content padding (16dp by default since containment.md C-12) shrinks every keyline near the ends, so large items resting at the start or the end carry a small inset. Bring-into-view took any inset over half a pixel for "not fully open", so focusing or pressing a large item at rest scrolled the row to it. The focal keyline's own inset at the current scroll is the threshold now. The press test also checks that focusing and pressing an open item moves nothing. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9 --- resources/js/carousel.js | 8 +++++++- tests/Browser/CarouselTest.php | 13 +++++++++++-- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/resources/js/carousel.js b/resources/js/carousel.js index c1260e8e..0d199027 100644 --- a/resources/js/carousel.js +++ b/resources/js/carousel.js @@ -808,6 +808,7 @@ document.addEventListener('alpine:init', () => { strategy: null, snaps: [], maxScroll: 0, + focalInset: 0, rtl: false, vertical: false, measured: false, @@ -1027,6 +1028,11 @@ document.addEventListener('alpine:init', () => { // moved or faded, so every item stays at strategy.itemSize. const still = state.reducedMotion.matches + // Content padding shrinks every keyline near the ends (Strategy.kt's + // createShiftedKeylineListForContentPadding), so an item at the focal size can still + // carry a small inset there: that inset, not zero, is "fully open" for isMasked(). + state.focalInset = still ? 0 : clamp((size - (firstFocal(keylines)?.size ?? size)) / 2, 0, size / 2) + state.items.forEach((item, index) => { const center = index * (size + strategy.itemSpacing) + size / 2 - scroll const before = keylineBefore(keylines, center) @@ -1220,7 +1226,7 @@ document.addEventListener('alpine:init', () => { return item.left < row.left - 1 || item.right > row.right + 1 } - return parseFloat(state.items[index].surface.style.getPropertyValue('--material-carousel-inset')) > 0.5 + return parseFloat(state.items[index].surface.style.getPropertyValue('--material-carousel-inset')) > state.focalInset + 0.5 }, destroy() { diff --git a/tests/Browser/CarouselTest.php b/tests/Browser/CarouselTest.php index 9b45d8f1..792f4761 100644 --- a/tests/Browser/CarouselTest.php +++ b/tests/Browser/CarouselTest.php @@ -145,9 +145,18 @@ it('snaps a scroll that stops between items onto an item', function () { JS)); }); -it('brings a partly hidden item into focus when it is pressed', function () { +it('brings a partly hidden item into focus when it is pressed, and leaves an open one where it is', function () { $page = carouselShowcase() - ->assertScript(onCarousel(0, 'return ! open(4)')); + ->assertScript(onCarousel(0, 'return open(1) && ! open(4)')); + + // Item 1 rests at the large size, short only of the content padding's share: focusing or + // pressing it moves nothing. + $page->script(onCarousel(0, 'items[1].focus(); items[1].querySelector(\'[data-material-carousel-content]\').click()')); + + $page->assertScript(onCarousel(0, <<<'JS' + await pause(400) + return scroller.scrollLeft === 0 && open(1) + JS)); $page->script(onCarousel(0, 'items[4].querySelector(\'[data-material-carousel-content]\').click()'));