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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
This commit is contained in:
Andreas Reinhold / reini
2026-09-14 13:36:26 +02:00
co-authored by Claude Opus 5
parent 0d6b0035ed
commit 56e985c40f
2 changed files with 18 additions and 3 deletions
+7 -1
View File
@@ -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() {
+11 -2
View File
@@ -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()'));