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

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:
Ilia Denisov
2026-06-16 07:18:50 +02:00
parent 115c92b39a
commit dd45af20ef
3 changed files with 40 additions and 5 deletions
+15
View File
@@ -135,6 +135,21 @@ describe('createBannerRotator', () => {
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)', () => {
vi.useFakeTimers();
const zero: BannerTimings = { ...cfg, fadeOutMs: 0, gapMs: 0, fadeInMs: 0 };
+8 -2
View File
@@ -138,8 +138,14 @@ export function createBannerRotator(
if (Date.now() - cycleStart >= config.holdMs) {
advance();
} else {
host.resetScroll();
scrollCycle(over);
// Loop the same message with a fade: fade out at the right edge, rewind to the start
// 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));
});
}
});
});