From d69c37089e6053f7ef43c3f1d3491840644fed75 Mon Sep 17 00:00:00 2001 From: Emi Matchu Date: Fri, 6 Sep 2024 12:13:10 -0700 Subject: [PATCH] Fix bug where item preview loading indicator sometimes doesn't delay The loading indicator *should* fade in after two seconds, to avoid a flash of a loading indicator when the page loads quickly - but in some circumstances it wouldn't delay: 1. Visit an item page. (It delays correctly the first time!) 2. Click "Infinite Closet", then click a link to another item page. 3. The loading indicator appears immediately, because this time the web component JS is already loaded, so the `outfit-layer` elements enter `:state(loading)` *immediately*. The element starts at `opacity: 1`, and the delay doesn't matter, because it was never at anything else. In this change, we have the `outfit-viewer` web component take on a `:state(after-first-frame)`, after a `setTimeout(0)` resolves. That enables the loading state CSS to *never* apply on the first frame, but then sometimes kick in on the *second* frame, so that the element is correctly perceived as "transitioning" from hidden to visible, and the two-second delay will apply. --- app/assets/javascripts/outfit-viewer.js | 4 ++++ app/assets/stylesheets/items/_show.sass | 21 +++++++++++++++------ 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/app/assets/javascripts/outfit-viewer.js b/app/assets/javascripts/outfit-viewer.js index 4e034f2d..b6d57a71 100644 --- a/app/assets/javascripts/outfit-viewer.js +++ b/app/assets/javascripts/outfit-viewer.js @@ -21,6 +21,10 @@ class OutfitViewer extends HTMLElement { this.#setIsPlaying(playPauseToggle.checked); this.#setIsPlayingCookie(playPauseToggle.checked); }); + + // Tell the CSS our first frame has rendered, which we use for loading + // state transitions. + this.#internals.states.add("after-first-frame"); } #setIsPlaying(isPlaying) { diff --git a/app/assets/stylesheets/items/_show.sass b/app/assets/stylesheets/items/_show.sass index 15a44ecc..92cc9de7 100644 --- a/app/assets/stylesheets/items/_show.sass +++ b/app/assets/stylesheets/items/_show.sass @@ -135,15 +135,24 @@ body.items-show margin-bottom: .5em display: none - // When loading, fade in the loading spinner after a brief delay. (We only - // apply the delay here, because fading *out* on load should be instant.) - // We are loading when the is busy, or when at least one layer + // When loading, fade in the loading spinner after a brief delay. We are + // loading when the is busy, or when at least one layer // is loading. + // + // We only apply the delay here, not on the base styles, because fading + // *out* on load should be instant. We also wait for the outfit-viewer to + // execute a `setTimeout(0)`, to make sure we always *start* in the + // non-loading state. This is because it's sometimes possible for the page to + // start with the web component already in `state(loading)`, and we need to + // make sure we *start* in *non-loading* state for the transition delay to + // happen. (This can happen when you Turbo-navigate between multiple items.) #item-preview[busy] outfit-viewer, outfit-viewer:has(outfit-layer:state(loading)) cursor: wait - .loading-indicator - opacity: 1 - transition-delay: 2s + + &:state(after-first-frame) + .loading-indicator + opacity: 1 + transition-delay: 2s #item-preview:has(outfit-layer:state(error)) outfit-viewer