Skip to content

Conversation

@pontusarmini
Copy link
Collaborator

Fix: Handle 0s transition duration & improve style cleanup on close

Summary

This PR addresses a critical issue where ad formats could get stuck in a "closing" state if the close animation duration was set to 0 (or if the user prefers reduced motion). It ensures that cleanup callbacks are executed immediately when no transition occurs, rather than waiting for a transitionend event that never fires.

Additionally, it refines the TopScroll close behavior to prevent visual glitches by deferring style resets until after the closing animation completes.

Key Changes

Core Wrapper Logic (src/advantage/wrapper.ts)

  • Smart Transition Detection: animateClose now checks getComputedStyle for transition-duration and transition-property.
  • Immediate Cleanup: If the duration is 0s or the property is none, the element is hidden and the callback is executed immediately, bypassing the transitionend listener.
  • Race Condition Guard: Added a check (if (this.currentFormat)) inside the transition callback to abort cleanup if a new format has started while the previous one was animating out.
  • Callback Support: animateClose now accepts an optional callback parameter to run logic after the animation (or immediate close) finishes.

Format Updates

TopScroll (src/advantage/formats/topscroll.ts)

  • Updated close to use the new animateClose callback.
  • Deferred Reset: CSS properties (like height and animation duration) and resetCSS() are now removed only after the animation finishes. This prevents the ad from snapping to a different state before sliding out.

WelcomePage (src/advantage/formats/welcomepage.ts & .css)

  • Zero-Duration Support: Implemented similar logic to the wrapper to detect if the container has a valid transition before waiting for transitionend.
  • Configurable Duration: Now supports closeButtonAnimationDuration by mapping it to a CSS variable (--adv-wp-transition-duration), allowing the animation speed to be controlled or disabled via config.

Testing

  • Added a new playground test page (playground/local-dev/welcomepage-test.html) to verify that WelcomePage closes immediately and correctly when closeButtonAnimationDuration is set to 0.

Checklist

  • TopScroll format animates out smoothly before resetting styles.
  • TopScroll format closes immediately if animation duration is 0.
  • WelcomePage format closes immediately if animation duration is 0.
  • No regression# Fix: Handle 0s transition duration & improve style cleanup on close

Summary

This PR addresses a critical issue where ad formats could get stuck in a "closing" state if the close animation duration was set to 0 (or if the user prefers reduced motion). It ensures that cleanup callbacks are executed immediately when no transition occurs, rather than waiting for a transitionend event that never fires.

Additionally, it refines the TopScroll close behavior to prevent visual glitches by deferring style resets until after the closing animation completes.

Key Changes

Core Wrapper Logic (src/advantage/wrapper.ts)

  • Smart Transition Detection: animateClose now checks getComputedStyle for transition-duration and transition-property.
  • Immediate Cleanup: If the duration is 0s or the property is none, the element is hidden and the callback is executed immediately, bypassing the transitionend listener.
  • Race Condition Guard: Added a check (if (this.currentFormat)) inside the transition callback to abort cleanup if a new format has started while the previous one was animating out.
  • Callback Support: animateClose now accepts an optional callback parameter to run logic after the animation (or immediate close) finishes.

Format Updates

TopScroll (src/advantage/formats/topscroll.ts)

  • Updated close to use the new animateClose callback.
  • Deferred Reset: CSS properties (like height and animation duration) and resetCSS() are now removed only after the animation finishes. This prevents the ad from snapping to a different state before sliding out.

WelcomePage (src/advantage/formats/welcomepage.ts & .css)

  • Zero-Duration Support: Implemented similar logic to the wrapper to detect if the container has a valid transition before waiting for transitionend.
  • Configurable Duration: Now supports closeButtonAnimationDuration by mapping it to a CSS variable (--adv-wp-transition-duration), allowing the animation speed to be controlled or disabled via config.

Testing

  • Added a new playground test page (playground/local-dev/welcomepage-test.html) to verify that WelcomePage closes immediately and correctly when closeButtonAnimationDuration is set to 0.

Checklist

  • TopScroll format animates out smoothly before resetting styles.
  • TopScroll format closes immediately if animation duration is 0.
  • WelcomePage format closes immediately if animation duration is 0.
  • No regression

…yle cleanup

- Update `AdvantageWrapper.animateClose` to detect if a transition will actually occur.
- Immediately execute cleanup callbacks if `transition-duration` is 0s or `transition-property` is 'none', preventing the wrapper from getting stuck waiting for a `transitionend` event that never fires.
- Fix `WelcomePage` format close logic to also handle 0s transition durations correctly.
- Update `TopScroll` format to remove CSS properties (height, animation duration) only *after* the closing animation completes (via callback), ensuring smooth transitions.
- Add `if (this.currentFormat)` check in `animateClose` to prevent race conditions if a new format is requested while the previous one is closing.
- Allow `WelcomePage` transitionfix: handle 0s transition duration in close animations and improve style cleanup

- Update `AdvantageWrapper.animateClose` to detect if a transition will actually occur.
- Immediately execute cleanup callbacks if `transition-duration` is 0s or `transition-property` is 'none', preventing the wrapper from getting stuck waiting for a `transitionend` event that never fires.
- Fix `WelcomePage` format close logic to also handle 0s transition durations correctly.
- Update `TopScroll` format to remove CSS properties (height, animation duration) only *after* the closing animation completes (via callback), ensuring smooth transitions.
- Add `if (this.currentFormat)` check in `animateClose` to prevent race conditions if a new format is requested while the previous one is closing.
- Allow `WelcomePage` transition
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes a critical issue where ad formats could become stuck in a "closing" state when transition animations are disabled (duration = 0s) or when users have reduced motion preferences. The fix detects zero-duration transitions and executes cleanup immediately rather than waiting for a transitionend event that never fires.

Key Changes:

  • Enhanced animateClose in wrapper.ts to detect transition duration and skip event listeners when duration is 0
  • Applied similar zero-duration detection to WelcomePage format's close method
  • Made WelcomePage's transition duration configurable via CSS variable
  • Added test page to verify zero-duration close behavior

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
src/advantage/wrapper.ts Added transition detection logic to animateClose method to handle zero-duration animations with immediate cleanup
src/advantage/formats/welcomepage.ts Implemented zero-duration transition detection in close method, added support for configurable animation duration via CSS variable, and added CSS variable cleanup in reset
src/advantage/formats/welcomepage.css Updated transition property to use CSS variable for configurable duration
playground/local-dev/welcomepage-test.ts Added test configuration file for WelcomePage with zero animation duration
playground/local-dev/welcomepage-test.html Added test HTML page to manually verify zero-duration close behavior
playground/local-dev/config.ts Added closeButtonAnimationDuration option to TopScroll format configuration for testing
Comments suppressed due to low confidence (1)

src/advantage/wrapper.ts:501

  • The transitionend event listener is added without removing it in case of errors or edge cases. While the once: true option ensures it fires only once, if a transition is cancelled or interrupted (e.g., element removed from DOM, CSS changed), the callback might never fire. Consider adding a safety timeout or checking if the element is still in the DOM before setting display to none.
        this.addEventListener(
            "transitionend",
            () => {
                // If a new format has started (currentFormat is set), abort cleanup
                if (this.currentFormat) {
                    return;
                }
                this.style.display = "none";
                this.classList.remove("animate");
                if (callback) {
                    callback();
                }
            },
            { once: true }
        );

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +143 to +157
if (container) {
const computedStyle = window.getComputedStyle(container);
const transitionProperty = computedStyle.transitionProperty;
const transitionDuration = computedStyle.transitionDuration;

const hasTransition =
transitionProperty !== "none" &&
transitionDuration.split(",").some((d) => parseFloat(d) > 0);

if (!hasTransition) {
wrapper.style.display = "none";
wrapper.classList.remove("show");
wrapper.style.height = "0px";
return;
}
Copy link

Copilot AI Dec 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The transition detection logic is duplicated between wrapper.ts (lines 467-475) and welcomepage.ts (lines 144-150). This creates a maintenance burden where bugs or improvements must be applied in multiple places. Consider extracting this logic into a shared utility function that both can use.

Copilot uses AI. Check for mistakes.
Comment on lines 30 to 39
return new Promise((resolve) => {
// Inser the CSS for the top scroll format
wrapper.insertCSS(varsCSS.concat(welcomepageCSS));
// Set the styles for the ad iframe
if (ad) {
setDimensionsUntilAdvantageAdSlot(ad);

if (config.closeButtonAnimationDuration !== undefined) {
wrapper.style.setProperty(
"--adv-wp-transition-duration",
`${config.closeButtonAnimationDuration}s`
);
}
Copy link

Copilot AI Dec 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The call to setDimensionsUntilAdvantageAdSlot(ad) was removed from the setup function. All other formats (topscroll.ts line 51, midscroll.ts line 15, multi-midscroll-base.ts line 69) call this function during setup, and welcomepage.ts still calls resetDimensionsUntilAdvantageAdSlot(ad) in its reset function (line 126). This removal appears unintentional and could break the ad dimensions setup for the WelcomePage format. The function call should be restored.

Copilot uses AI. Check for mistakes.
Comment on lines +477 to +480
if (!hasTransition) {
this.classList.remove("animate");
this.style.height = "0px";
this.style.display = "none";
Copy link

Copilot AI Dec 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the immediate cleanup path (no transition), the code sets style.height = "0px" before style.display = "none". This could cause a brief visual flicker if the element is still visible when height is set to 0. Consider setting display = "none" first, or remove the height setting since it's redundant when display is none. The transitioned path (line 502) sets height after setting up the listener, which is correct for animation.

Copilot uses AI. Check for mistakes.
Comment on lines +143 to +161
if (container) {
const computedStyle = window.getComputedStyle(container);
const transitionProperty = computedStyle.transitionProperty;
const transitionDuration = computedStyle.transitionDuration;

const hasTransition =
transitionProperty !== "none" &&
transitionDuration.split(",").some((d) => parseFloat(d) > 0);

if (!hasTransition) {
wrapper.style.display = "none";
wrapper.classList.remove("show");
wrapper.style.height = "0px";
return;
}

container.addEventListener("transitionend", handleTransitionEnd);
}

Copy link

Copilot AI Dec 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When the container is not found (null or undefined), the function silently continues to execute lines 162-163, which set wrapper styles. This could lead to unexpected behavior where the wrapper's height is set to 0px and the "show" class is removed even though no container was found. Consider returning early or handling the null case explicitly, similar to how the hasTransition check returns early on line 156.

Copilot uses AI. Check for mistakes.
@pattan
Copy link
Contributor

pattan commented Dec 17, 2025

What copilot said :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants