feat: product thumbnail hover/click swap — capability-driven enhancement
New /static/js/product-thumbnail-swap.js (loaded with `defer` only when thumbnail1 is present in product.extensions): hovering a thumbnail swaps the .product-main preview image to that thumb's source; clicking sticks the swap and preventDefault()s the anchor so the customer stays on the product page. Mouseleave on the .product-images container restores the cover image unless a click has pinned it. The no-JS path is unchanged — each thumbnail stays wrapped in an <a href target="_blank"> so clicking still opens the full image in a new tab when the script isn't loaded. The script self-bails on non-img main elements (watch-mode video / audio art) and on .audio-cover so album-art swap doesn't fire on audio products. CSS: .product-thumbnail gains a brief border-color transition + a :hover navy outline so the affordance reads without changing markup. Test: asset-served check for the new JS file. The template wiring is a single guarded line; if it regresses, the visual smoke test on the product page catches it.
This commit is contained in:
parent
873cb375a5
commit
4e203c4001
4 changed files with 90 additions and 0 deletions
|
|
@ -635,6 +635,13 @@ img.product-thumbnail {
|
|||
max-height:58px;
|
||||
width: auto;
|
||||
height: auto;
|
||||
transition: border-color var(--duration-fast, 100ms) var(--ease-standard, ease);
|
||||
}
|
||||
/* Lit-up affordance when the JS hover-swap enhancement is active —
|
||||
product-thumbnail-swap.js binds hover/click handlers; the visual cue
|
||||
ships in CSS so it works without changing the markup. */
|
||||
.product-images img.product-thumbnail:hover {
|
||||
border-color: var(--color-navy, #5871ad);
|
||||
}
|
||||
|
||||
.product-main {
|
||||
|
|
|
|||
65
make_post_sell/static/js/product-thumbnail-swap.js
Normal file
65
make_post_sell/static/js/product-thumbnail-swap.js
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
/* Product thumbnail swap — capability-driven enhancement.
|
||||
*
|
||||
* No JS: each thumbnail under .product-images is wrapped in an
|
||||
* <a href="{thumb_url}" target="_blank"> so a click opens the full
|
||||
* image in a new tab — that path is unchanged.
|
||||
*
|
||||
* With JS: hovering a thumbnail swaps the main image preview to that
|
||||
* thumbnail's source; clicking sticks the swap and suppresses the
|
||||
* default navigation (so the customer stays on the product page and
|
||||
* keeps cycling through views, Amazon-style).
|
||||
*
|
||||
* Bails silently when:
|
||||
* - there's no .product-images container
|
||||
* - the main element isn't an <img> (watch-mode video / audio art)
|
||||
* - .product-main is the audio-cover (swapping album art on hover
|
||||
* would be confusing)
|
||||
* - there are no .product-thumbnail children
|
||||
*/
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
var container = document.querySelector(".product-images");
|
||||
if (!container) return;
|
||||
|
||||
var mainEl = container.querySelector("img.product-main");
|
||||
if (!mainEl) return;
|
||||
if (mainEl.classList.contains("audio-cover")) return;
|
||||
|
||||
var thumbs = container.querySelectorAll("a > img.product-thumbnail");
|
||||
if (thumbs.length === 0) return;
|
||||
|
||||
// Remember the original src so we never lose the cover image; on
|
||||
// mouseleave of the whole strip we restore it (matches expected
|
||||
// "hover to preview, leave to see the cover again" behavior).
|
||||
var originalSrc = mainEl.getAttribute("src");
|
||||
var sticky = false; // a click pins the swap; mouseleave won't restore.
|
||||
|
||||
function swapTo(url) {
|
||||
if (url && mainEl.getAttribute("src") !== url) {
|
||||
mainEl.setAttribute("src", url);
|
||||
}
|
||||
}
|
||||
|
||||
Array.prototype.forEach.call(thumbs, function (img) {
|
||||
var anchor = img.parentElement;
|
||||
|
||||
img.addEventListener("mouseenter", function () {
|
||||
swapTo(img.src);
|
||||
});
|
||||
|
||||
if (anchor && anchor.tagName === "A") {
|
||||
anchor.addEventListener("click", function (e) {
|
||||
e.preventDefault();
|
||||
sticky = true;
|
||||
swapTo(img.src);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
container.addEventListener("mouseleave", function () {
|
||||
if (!sticky && originalSrc) {
|
||||
swapTo(originalSrc);
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
|
@ -132,6 +132,12 @@
|
|||
<a href="{{ get_endpoint }}" target="_blank"><img src="{{ get_endpoint }}?ts={{ product.updated_timestamp }}" class="product-thumbnail" /></a>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% if "thumbnail1" in product.extensions %}
|
||||
{# Capability-driven enhancement: hover/click a thumbnail to swap
|
||||
the main preview. The no-JS path keeps the original
|
||||
<a target="_blank"> open-in-new-tab behavior. #}
|
||||
<script src="/static/js/product-thumbnail-swap.js?v={{ request.git_hash }}" defer></script>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="cinema-content-stack">
|
||||
|
|
|
|||
|
|
@ -7307,6 +7307,18 @@ class TestOfferCheckout(_AuthenticatedBase):
|
|||
self.assertIn("auction-page", body)
|
||||
self.assertIn("auction-countdown", body)
|
||||
|
||||
def test_product_thumbnail_swap_js_served(self):
|
||||
"""Capability-driven hover/click swap of the product cover image
|
||||
against the thumbnail strip — the JS itself is asset-tested
|
||||
here; the template wires it only when thumbnail1 is present."""
|
||||
res = self.testapp.get(
|
||||
"/static/js/product-thumbnail-swap.js", status=200,
|
||||
)
|
||||
body = res.body.decode()
|
||||
self.assertIn(".product-images", body)
|
||||
self.assertIn(".product-thumbnail", body)
|
||||
self.assertIn("mouseenter", body)
|
||||
|
||||
def test_auction_page_loads_auction_js(self):
|
||||
"""MPS-20: auction.j2 references /static/js/auction.js."""
|
||||
from ..models.auction import (
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue