Leave carousel items unmasked under reduced motion
Plan step 12, containment.md C-05. Only the parallax half of M3's rule was honoured: the mask was still written every frame, so items kept growing and shrinking between the keylines. Under reduced motion the inset, the shift and the label fade are now all zero, so every item stays at the strategy's large size and the keylines only decide where the row snaps. `--material-carousel-pin` went with it: with no mask there is nothing to pin the content to. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
This commit is contained in:
co-authored by
Claude Opus 5
parent
335ea4f4f1
commit
f895b553b6
+15
-11
@@ -23,9 +23,10 @@
|
|||||||
* records. A ResizeObserver does the same for a new width. RTL is read when measuring:
|
* records. A ResizeObserver does the same for a new width. RTL is read when measuring:
|
||||||
* scroll offsets are negative there and shifts mirror, as Compose's `translationX` does.
|
* scroll offsets are negative there and shifts mirror, as Compose's `translationX` does.
|
||||||
*
|
*
|
||||||
* Under reduced motion the buttons and keys scroll instantly, and the content stays pinned to
|
* Under reduced motion the buttons and keys scroll instantly and nothing is masked at all: M3
|
||||||
* the mask's leading edge instead of sliding inside it: the frame still opens and closes with
|
* says the parallax goes and items "should no longer expand as they come into view — all items
|
||||||
* the scroll the user makes, but nothing moves on its own.
|
* are the same size", so every item stays at the strategy's large size and the keylines only
|
||||||
|
* decide where the row snaps.
|
||||||
*
|
*
|
||||||
* ---------------------------------------------------------------------------------------
|
* ---------------------------------------------------------------------------------------
|
||||||
* Keyline maths ported from androidx (https://github.com/androidx/androidx), commit
|
* Keyline maths ported from androidx (https://github.com/androidx/androidx), commit
|
||||||
@@ -872,7 +873,6 @@ document.addEventListener('alpine:init', () => {
|
|||||||
.map((element) => ({
|
.map((element) => ({
|
||||||
element,
|
element,
|
||||||
surface: element.querySelector('[data-material-carousel-surface]'),
|
surface: element.querySelector('[data-material-carousel-surface]'),
|
||||||
content: element.querySelector('[data-material-carousel-content]'),
|
|
||||||
label: element.querySelector('[data-material-carousel-label]'),
|
label: element.querySelector('[data-material-carousel-label]'),
|
||||||
labelWidth: 0,
|
labelWidth: 0,
|
||||||
written: '',
|
written: '',
|
||||||
@@ -963,7 +963,13 @@ document.addEventListener('alpine:init', () => {
|
|||||||
const scroll = this.scrollOffset()
|
const scroll = this.scrollOffset()
|
||||||
const keylines = keylinesForScrollOffset(strategy, scroll, state.maxScroll)
|
const keylines = keylinesForScrollOffset(strategy, scroll, state.maxScroll)
|
||||||
const size = strategy.itemSize
|
const size = strategy.itemSize
|
||||||
const pinned = state.reducedMotion.matches
|
|
||||||
|
// M3: "When reduced motion settings are turned on, the parallax effect should be
|
||||||
|
// removed and carousel items should no longer expand as they come into view. All
|
||||||
|
// items are the same size" (docs/reference/m3/styles.md § Motion → Accessibility
|
||||||
|
// requirements). The keylines still decide where the row snaps; nothing is masked,
|
||||||
|
// moved or faded, so every item stays at strategy.itemSize.
|
||||||
|
const still = state.reducedMotion.matches
|
||||||
|
|
||||||
state.items.forEach((item, index) => {
|
state.items.forEach((item, index) => {
|
||||||
const center = index * (size + strategy.itemSpacing) + size / 2 - scroll
|
const center = index * (size + strategy.itemSpacing) + size / 2 - scroll
|
||||||
@@ -978,11 +984,10 @@ document.addEventListener('alpine:init', () => {
|
|||||||
translation += (center - keyline.unadjustedOffset) / keyline.size
|
translation += (center - keyline.unadjustedOffset) / keyline.size
|
||||||
}
|
}
|
||||||
|
|
||||||
const inset = clamp((size - keyline.size) / 2, 0, size / 2)
|
const inset = still ? 0 : clamp((size - keyline.size) / 2, 0, size / 2)
|
||||||
const shift = state.rtl ? -translation : translation
|
const shift = still ? 0 : state.rtl ? -translation : translation
|
||||||
const pin = pinned ? (state.rtl ? -inset : inset) : 0
|
const opacity = still || item.labelWidth === 0 ? 1 : clamp((item.labelWidth - size + keyline.size) / item.labelWidth, 0, 1)
|
||||||
const opacity = item.labelWidth > 0 ? clamp((item.labelWidth - size + keyline.size) / item.labelWidth, 0, 1) : 1
|
const written = `${inset.toFixed(2)}|${shift.toFixed(2)}|${opacity.toFixed(3)}`
|
||||||
const written = `${inset.toFixed(2)}|${shift.toFixed(2)}|${pin.toFixed(2)}|${opacity.toFixed(3)}`
|
|
||||||
|
|
||||||
if (written === item.written) {
|
if (written === item.written) {
|
||||||
return
|
return
|
||||||
@@ -993,7 +998,6 @@ document.addEventListener('alpine:init', () => {
|
|||||||
item.written = written
|
item.written = written
|
||||||
item.surface.style.setProperty('--material-carousel-inset', `${inset.toFixed(2)}px`)
|
item.surface.style.setProperty('--material-carousel-inset', `${inset.toFixed(2)}px`)
|
||||||
item.surface.style.setProperty('--material-carousel-shift', `${shift.toFixed(2)}px`)
|
item.surface.style.setProperty('--material-carousel-shift', `${shift.toFixed(2)}px`)
|
||||||
item.surface.style.setProperty('--material-carousel-pin', `${pin.toFixed(2)}px`)
|
|
||||||
item.surface.style.setProperty('--material-carousel-label-shift', `${(state.rtl ? -inset : inset).toFixed(2)}px`)
|
item.surface.style.setProperty('--material-carousel-label-shift', `${(state.rtl ? -inset : inset).toFixed(2)}px`)
|
||||||
item.surface.style.setProperty('--material-carousel-label', opacity.toFixed(3))
|
item.surface.style.setProperty('--material-carousel-label', opacity.toFixed(3))
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -34,7 +34,7 @@
|
|||||||
data-material-carousel-surface
|
data-material-carousel-surface
|
||||||
class="relative size-full overflow-hidden rounded-corner-xl bg-surface-container-highest text-on-surface translate-x-(--material-carousel-shift) [clip-path:inset(0_var(--material-carousel-inset,0px)_round_var(--md-sys-shape-corner-xl))]"
|
class="relative size-full overflow-hidden rounded-corner-xl bg-surface-container-highest text-on-surface translate-x-(--material-carousel-shift) [clip-path:inset(0_var(--material-carousel-inset,0px)_round_var(--md-sys-shape-corner-xl))]"
|
||||||
>
|
>
|
||||||
<div data-material-carousel-content class="size-full translate-x-(--material-carousel-pin) [&>img]:size-full [&>img]:object-cover">
|
<div data-material-carousel-content class="size-full [&>img]:size-full [&>img]:object-cover">
|
||||||
{{ $slot }}
|
{{ $slot }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -40,8 +40,10 @@
|
|||||||
keys move one item, Home and End to the ends; focus moving into an item, or a press on one
|
keys move one item, Home and End to the ends; focus moving into an item, or a press on one
|
||||||
that is not fully open, brings it into focus. `controls` adds previous and next icon
|
that is not fully open, brings it into focus. `controls` adds previous and next icon
|
||||||
buttons under the row — by default only where the pointer is fine (a mouse or trackpad);
|
buttons under the row — by default only where the pointer is fine (a mouse or trackpad);
|
||||||
`true` always, `false` never. Under reduced motion they scroll instantly and content no
|
`true` always, `false` never. Under reduced motion they scroll instantly and nothing is
|
||||||
longer slides inside its mask. RTL mirrors the keylines, keys and buttons.
|
masked: every item stays at its large size, which is M3's rule for a carousel under reduced
|
||||||
|
motion (docs/reference/m3/styles.md § Motion → Accessibility requirements). RTL mirrors the
|
||||||
|
keylines, keys and buttons.
|
||||||
|
|
||||||
Re-measures itself when resized, when a Livewire morph resets its styles and when items
|
Re-measures itself when resized, when a Livewire morph resets its styles and when items
|
||||||
come and go. The row's id, which the buttons control, is new with every render; the row
|
come and go. The row's id, which the buttons control, is new with every render; the row
|
||||||
|
|||||||
@@ -146,15 +146,14 @@ it('brings a partly hidden item into focus when it is pressed', function () {
|
|||||||
$page->assertScript(onCarousel(0, 'return inset(4) < 0.5 && scroller.scrollLeft > 0'));
|
$page->assertScript(onCarousel(0, 'return inset(4) < 0.5 && scroller.scrollLeft > 0'));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('scrolls instantly and keeps content pinned to its mask under reduced motion', function () {
|
it('scrolls instantly and leaves every item unmasked under reduced motion', function () {
|
||||||
carouselShowcase(['reducedMotion' => 'reduce'])
|
carouselShowcase(['reducedMotion' => 'reduce'])
|
||||||
->assertScript(onCarousel(0, <<<'JS'
|
->assertScript(onCarousel(0, <<<'JS'
|
||||||
root.querySelector('[aria-label="Next"]').click()
|
root.querySelector('[aria-label="Next"]').click()
|
||||||
const arrived = at(1)
|
const arrived = at(1)
|
||||||
await pause(50)
|
await pause(50)
|
||||||
const last = items.length - 1
|
return arrived && items.every((_, i) => inset(i) === 0)
|
||||||
return arrived && inset(last) > 0.5
|
&& items.every((item) => Math.abs(item.getBoundingClientRect().width - size) < 1.5)
|
||||||
&& surface(last).style.getPropertyValue('--material-carousel-pin') === surface(last).style.getPropertyValue('--material-carousel-inset')
|
|
||||||
JS));
|
JS));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user