script nits: slider hover/visibility pause + silent autoplay + iframe amp guard
ContentSlider: autoplay's setInterval now pauses on mouseenter and on document visibilitychange (tab hidden), resumes on mouseleave/visible, and is always clearable (timer var + clearInterval). The aria-live region is "off" while autoplay is silently auto-rotating and only flips to "polite" on manual next/prev/dot navigation, so screen readers aren't spammed with an announcement every `interval` ms (F-export review Minor). Updates the one existing test that hard-coded aria-live="polite" as always-on to match this intentional behavior change. (Countdown's ticking-interval-stops-at-zero nit shipped in the previous commit alongside its node-id migration, since both touched the same inline script.) Adds regression tests locking in that MapEmbed/VideoBlock iframe src attributes (built by string concatenation with literal `&` query params) are HTML-entity-encoded via the existing escapeAttr(safeUrl()) pipeline -- verified already correct, no source change needed there. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import React, { CSSProperties, useState, useEffect, useRef, useCallback } from 'react';
|
||||
import { useNode, UserComponent } from '@craftjs/core';
|
||||
import { cssPropsToString } from '../../utils/style-helpers';
|
||||
import { escapeHtml, escapeAttr, safeUrl } from '../../utils/escape';
|
||||
import { escapeHtml, escapeAttr, safeUrl, scopeId } from '../../utils/escape';
|
||||
|
||||
interface Slide {
|
||||
type: 'image' | 'content';
|
||||
@@ -238,7 +238,7 @@ ContentSlider.craft = {
|
||||
|
||||
/* ---------- HTML export ---------- */
|
||||
|
||||
(ContentSlider as any).toHtml = (props: ContentSliderProps, _childrenHtml: string) => {
|
||||
(ContentSlider as any).toHtml = (props: ContentSliderProps, _childrenHtml: string, nodeId?: string) => {
|
||||
const {
|
||||
slides = defaultSlides,
|
||||
autoplay = true,
|
||||
@@ -250,7 +250,11 @@ ContentSlider.craft = {
|
||||
} = props;
|
||||
|
||||
const items = slides.length > 0 ? slides : defaultSlides;
|
||||
const uid = 'cs_' + Math.random().toString(36).slice(2, 8);
|
||||
// Deterministic AND unique id, scoped on the Craft node id, for this
|
||||
// slider's slide/dot element ids and inline-script globals -- so two
|
||||
// ContentSlider instances (e.g. both left at default slides) don't
|
||||
// collide and end up driving each other's rotation.
|
||||
const uid = scopeId(nodeId, JSON.stringify(items) + interval, 'cs');
|
||||
|
||||
const sectionStyle = cssPropsToString({
|
||||
position: 'relative',
|
||||
@@ -299,9 +303,18 @@ ContentSlider.craft = {
|
||||
</div>`
|
||||
: '';
|
||||
|
||||
// Autoplay ticks call show() directly (internal), while manual nav goes
|
||||
// through the exposed window[...] functions -- that split lets us mark
|
||||
// the live region "polite" only on manual navigation, and keep it "off"
|
||||
// while autoplay is silently auto-rotating, so screen readers aren't
|
||||
// spammed with an announcement every `interval` ms (F-export review
|
||||
// Minor).
|
||||
const autoplayActive = autoplay && items.length > 1;
|
||||
const liveAttr = autoplayActive ? 'off' : 'polite';
|
||||
|
||||
return {
|
||||
html: `<section${sectionStyle ? ` style="${sectionStyle}"` : ''}>
|
||||
<div aria-live="polite">
|
||||
html: `<section id="${uid}"${sectionStyle ? ` style="${sectionStyle}"` : ''}>
|
||||
<div id="${uid}_live" aria-live="${liveAttr}">
|
||||
${slidesHtml}
|
||||
</div>
|
||||
${arrowsHtml}
|
||||
@@ -309,6 +322,8 @@ ContentSlider.craft = {
|
||||
<script>
|
||||
(function(){
|
||||
var current=0, total=${items.length}, uid="${uid}";
|
||||
var liveRegion=document.getElementById(uid+"_live");
|
||||
function markManualNav(){ if(liveRegion){ liveRegion.setAttribute("aria-live","polite"); } }
|
||||
function show(idx){
|
||||
document.getElementById(uid+"_s"+current).style.opacity="0";
|
||||
${showDots ? `document.getElementById(uid+"_d"+current).style.backgroundColor="rgba(255,255,255,0.5)";` : ''}
|
||||
@@ -316,10 +331,23 @@ ContentSlider.craft = {
|
||||
document.getElementById(uid+"_s"+current).style.opacity="1";
|
||||
${showDots ? `document.getElementById(uid+"_d"+current).style.backgroundColor="#ffffff";` : ''}
|
||||
}
|
||||
window["${uid}_go"]=show;
|
||||
window["${uid}_next"]=function(){show(current+1);};
|
||||
window["${uid}_prev"]=function(){show(current-1);};
|
||||
${autoplay && items.length > 1 ? `setInterval(function(){show(current+1);},${interval});` : ''}
|
||||
window["${uid}_go"]=function(idx){ markManualNav(); show(idx); };
|
||||
window["${uid}_next"]=function(){ markManualNav(); show(current+1); };
|
||||
window["${uid}_prev"]=function(){ markManualNav(); show(current-1); };
|
||||
${autoplayActive ? `
|
||||
var timer=null;
|
||||
function start(){ if(!timer && document.visibilityState!=="hidden"){ timer=setInterval(function(){show(current+1);},${interval}); } }
|
||||
function stop(){ if(timer){ clearInterval(timer); timer=null; } }
|
||||
var root=document.getElementById(uid);
|
||||
if(root){
|
||||
root.addEventListener("mouseenter", stop);
|
||||
root.addEventListener("mouseleave", start);
|
||||
}
|
||||
document.addEventListener("visibilitychange", function(){
|
||||
if(document.visibilityState==="hidden"){ stop(); } else { start(); }
|
||||
});
|
||||
start();
|
||||
` : ''}
|
||||
})();
|
||||
</script>
|
||||
</section>`,
|
||||
|
||||
Reference in New Issue
Block a user