fix(ads): fade the scroll rewind + keep the banner strip height constant
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 13s
CI / ui (pull_request) Successful in 49s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m3s
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 9s
CI / integration (pull_request) Successful in 13s
CI / ui (pull_request) Successful in 49s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m3s
Two polish fixes (owner feedback): - Scroll loop: a long message that scrolled to its right edge rewound with a hard jump (no fade). It now runs the same fade as a message change at each rewind: fade out at the edge, reset the scroll while hidden, fade the same message back in, then scroll again. - Strip height: during the fade gap the message layer is removed, which let the strip collapse by ~1-2px. An always-present invisible spacer now reserves one line of height and the message is overlaid absolutely, so the strip height is constant whether or not the message is showing. Verified live: opacity sampling shows a full fade-out → gap → fade-in at each scroll rewind (~every 6s), and the .ad height stays a single constant value (30.31px) across the whole cycle including the gap. Loop-fade unit-tested.
This commit is contained in:
@@ -116,6 +116,10 @@
|
|||||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||||
<div class="ad" bind:this={viewport} onclick={onExternalLinkClick}>
|
<div class="ad" bind:this={viewport} onclick={onExternalLinkClick}>
|
||||||
|
<!-- An always-present, invisible spacer reserves exactly one line of height, so the strip never
|
||||||
|
collapses while the message layer is absent during the fade gap (the message is overlaid
|
||||||
|
absolutely, so its presence/absence does not change the strip height). -->
|
||||||
|
<span class="reserve" aria-hidden="true"> </span>
|
||||||
{#if visible}
|
{#if visible}
|
||||||
<div class="fadewrap" in:inFade out:fade={{ duration: eff.fadeOutMs }}>
|
<div class="fadewrap" in:inFade out:fade={{ duration: eff.fadeOutMs }}>
|
||||||
<div
|
<div
|
||||||
@@ -131,6 +135,7 @@
|
|||||||
|
|
||||||
<style>
|
<style>
|
||||||
.ad {
|
.ad {
|
||||||
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
padding: 6px 0;
|
padding: 6px 0;
|
||||||
@@ -142,9 +147,18 @@
|
|||||||
border-top: 1px solid var(--border);
|
border-top: 1px solid var(--border);
|
||||||
border-bottom: 1px solid var(--border);
|
border-bottom: 1px solid var(--border);
|
||||||
user-select: none;
|
user-select: none;
|
||||||
/* A stable height so the strip does not collapse during the fade gap (the message layer is
|
}
|
||||||
removed between fade-out and fade-in). */
|
/* Reserves one line of height inside the padding so .ad keeps a constant height even during the
|
||||||
min-height: calc(0.85rem * 1.2 + 12px);
|
fade gap; invisible and non-interactive. */
|
||||||
|
.reserve {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
/* The message layer is overlaid on the reserved line, so showing/hiding it never resizes .ad. */
|
||||||
|
.fadewrap {
|
||||||
|
position: absolute;
|
||||||
|
top: 6px;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
}
|
}
|
||||||
.track {
|
.track {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
|||||||
@@ -135,6 +135,21 @@ describe('createBannerRotator', () => {
|
|||||||
r.stop();
|
r.stop();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('fades the scroll loop of a long message (out, rewind, in) instead of a hard reset', () => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
const c: BannerTimings = { holdMs: 30000, edgePauseMs: 100, scrollPxPerSec: 1000, fadeOutMs: 100, gapMs: 50, fadeInMs: 100 };
|
||||||
|
const { host, calls } = recordingHost(200); // overflows -> scroll dur = 200ms
|
||||||
|
const r = createBannerRotator([{ weight: 1, messages: ['long'] }], host, c);
|
||||||
|
r.start();
|
||||||
|
// fadeIn -> measure -> edgePause -> scrollTo -> (dur + edgePause) -> loop boundary
|
||||||
|
vi.advanceTimersByTime(c.fadeInMs + c.edgePauseMs + 200 + c.edgePauseMs + 5);
|
||||||
|
expect(calls).toContain('scroll');
|
||||||
|
expect(calls).toContain('hide'); // the loop fades out at the right edge (not a hard reset)
|
||||||
|
vi.advanceTimersByTime(c.fadeOutMs + c.gapMs + 5); // -> the same message fades back in
|
||||||
|
expect(calls.filter((x) => x === 'show').length).toBeGreaterThanOrEqual(2);
|
||||||
|
r.stop();
|
||||||
|
});
|
||||||
|
|
||||||
it('swaps instantly when fade timings are zero (reduce-motion)', () => {
|
it('swaps instantly when fade timings are zero (reduce-motion)', () => {
|
||||||
vi.useFakeTimers();
|
vi.useFakeTimers();
|
||||||
const zero: BannerTimings = { ...cfg, fadeOutMs: 0, gapMs: 0, fadeInMs: 0 };
|
const zero: BannerTimings = { ...cfg, fadeOutMs: 0, gapMs: 0, fadeInMs: 0 };
|
||||||
|
|||||||
@@ -138,8 +138,14 @@ export function createBannerRotator(
|
|||||||
if (Date.now() - cycleStart >= config.holdMs) {
|
if (Date.now() - cycleStart >= config.holdMs) {
|
||||||
advance();
|
advance();
|
||||||
} else {
|
} else {
|
||||||
host.resetScroll();
|
// Loop the same message with a fade: fade out at the right edge, rewind to the start
|
||||||
scrollCycle(over);
|
// while hidden, fade the same message back in, then scroll again — so the rewind gets
|
||||||
|
// the same fade as a message change, not a hard jump.
|
||||||
|
host.hide(config.fadeOutMs);
|
||||||
|
at(config.fadeOutMs + config.gapMs, () => {
|
||||||
|
host.show(lastShown);
|
||||||
|
at(config.fadeInMs, () => scrollCycle(over));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user