Skip to content

Commit 0deeacb

Browse files
Merge pull request #871 from stellarwp/KAD-4904_count_down_pause_button
Countdown Accessibility Changes
2 parents e6ebd33 + 46e9944 commit 0deeacb

File tree

9 files changed

+860
-5
lines changed

9 files changed

+860
-5
lines changed

includes/assets/js/kb-countdown.min.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

includes/blocks/class-kadence-blocks-countdown-block.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -657,7 +657,8 @@ public function register_scripts() {
657657
return;
658658
}
659659

660-
wp_register_script( 'kadence-blocks-countdown', KADENCE_BLOCKS_URL . 'includes/assets/js/kb-countdown.min.js', array(), KADENCE_BLOCKS_VERSION, true );
660+
wp_register_script( 'kadence-blocks-countdown', KADENCE_BLOCKS_URL . 'includes/assets/js/kb-countdown.min.js', array( 'wp-i18n' ), KADENCE_BLOCKS_VERSION, true );
661+
wp_set_script_translations( 'kadence-blocks-countdown', 'kadence-blocks' );
661662
}
662663

663664

src/assets/js/kb-countdown.js

Lines changed: 200 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
timers: JSON.parse(kadence_blocks_countdown.timers),
77
listenerCache: {},
88
sliderEventCache: {},
9+
prefersReducedMotion: window.matchMedia('(prefers-reduced-motion: reduce)').matches,
910
isInViewport(el) {
1011
const rect = el.getBoundingClientRect();
1112
return (
@@ -168,7 +169,32 @@
168169
}
169170
return element;
170171
},
172+
announceToScreenReader(message) {
173+
// Create or get the live region for announcements
174+
let liveRegion = document.getElementById('kb-countdown-announcements');
175+
if (!liveRegion) {
176+
liveRegion = document.createElement('div');
177+
liveRegion.id = 'kb-countdown-announcements';
178+
liveRegion.setAttribute('role', 'status');
179+
liveRegion.setAttribute('aria-live', 'polite');
180+
liveRegion.setAttribute('aria-atomic', 'true');
181+
liveRegion.className = 'screen-reader-text';
182+
liveRegion.style.cssText =
183+
'position: absolute; left: -10000px; width: 1px; height: 1px; overflow: hidden;';
184+
document.body.appendChild(liveRegion);
185+
}
186+
// Clear previous message and set new one
187+
liveRegion.textContent = '';
188+
setTimeout(() => {
189+
liveRegion.textContent = message;
190+
}, 100);
191+
},
171192
updateTimerInterval(element, id, parent) {
193+
// Check if paused
194+
if (this.cache[id] && this.cache[id].paused) {
195+
return;
196+
}
197+
172198
const currentTimeStamp = new Date();
173199
const userTimezoneOffset = -1 * (new Date().getTimezoneOffset() / 60);
174200
let total = '';
@@ -317,6 +343,10 @@
317343
window.location.href = window.kadenceCountdown.timers[id].redirect;
318344
}
319345
} else if ('hide' === window.kadenceCountdown.timers[id].action) {
346+
// Announce to screen readers before hiding
347+
window.kadenceCountdown.announceToScreenReader(
348+
wp.i18n.__('Countdown timer has ended.', 'kadence-blocks')
349+
);
320350
parent.style.display = 'none';
321351
} else if ('message' === window.kadenceCountdown.timers[id].action) {
322352
if (parent.querySelector('.kb-countdown-inner-first')) {
@@ -328,8 +358,26 @@
328358
if (parent.querySelector('.kb-countdown-inner-second')) {
329359
parent.querySelector('.kb-countdown-inner-second').style.display = 'none';
330360
}
331-
if (parent.querySelector('.kb-countdown-inner-complete')) {
332-
parent.querySelector('.kb-countdown-inner-complete').style.display = 'block';
361+
const completeContent = parent.querySelector('.kb-countdown-inner-complete');
362+
if (completeContent) {
363+
completeContent.style.display = 'block';
364+
// Make the replacement content a live region for screen readers
365+
completeContent.setAttribute('role', 'status');
366+
completeContent.setAttribute('aria-live', 'polite');
367+
completeContent.setAttribute('aria-atomic', 'true');
368+
// Announce the content change
369+
const completeText = window.kadenceCountdown.stripHtml(
370+
completeContent.textContent || completeContent.innerText || ''
371+
);
372+
if (completeText.trim()) {
373+
window.kadenceCountdown.announceToScreenReader(
374+
wp.i18n.__('Countdown timer has ended.', 'kadence-blocks') + ' ' + completeText
375+
);
376+
} else {
377+
window.kadenceCountdown.announceToScreenReader(
378+
wp.i18n.__('Countdown timer has ended. New content is now available.', 'kadence-blocks')
379+
);
380+
}
333381
}
334382
parent.style.opacity = 1;
335383
if (window.kadenceCountdown.timers[id].revealOnLoad) {
@@ -585,7 +633,9 @@
585633
if (sticky && !window.kadenceCountdown.timers[id].timer) {
586634
setTimeout(function () {
587635
parent.style.height = parent.scrollHeight + 'px';
588-
sticky.style.transition = 'height 0.8s ease';
636+
if (!window.kadenceCountdown.prefersReducedMotion) {
637+
sticky.style.transition = 'height 0.8s ease';
638+
}
589639
sticky.style.height = Math.floor(sticky.scrollHeight + parent.scrollHeight) + 'px';
590640
}, 200);
591641
setTimeout(function () {
@@ -632,12 +682,84 @@
632682
{ element: slider, event: 'splide:moved', handler: movedHandler },
633683
];
634684
},
685+
togglePause(id, button) {
686+
if (!window.kadenceCountdown.cache[id]) {
687+
return;
688+
}
689+
690+
const isPaused = window.kadenceCountdown.cache[id].paused || false;
691+
692+
if (isPaused) {
693+
// Resume
694+
window.kadenceCountdown.cache[id].paused = false;
695+
button.setAttribute('aria-pressed', 'false');
696+
button.setAttribute('aria-label', wp.i18n.__('Pause countdown timer', 'kadence-blocks'));
697+
button.setAttribute('title', wp.i18n.__('Pause countdown', 'kadence-blocks'));
698+
const icon = button.querySelector('.kb-countdown-pause-icon');
699+
if (icon) {
700+
icon.innerHTML =
701+
'<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><rect x="6" y="4" width="4" height="16" rx="1" fill="currentColor" /><rect x="14" y="4" width="4" height="16" rx="1" fill="currentColor" /></svg>';
702+
}
703+
button.classList.remove('kb-countdown-paused');
704+
705+
// Restart the interval
706+
if (window.kadenceCountdown.cache[id].element && window.kadenceCountdown.cache[id].parent) {
707+
// Update immediately
708+
window.kadenceCountdown.updateTimerInterval(
709+
window.kadenceCountdown.cache[id].element,
710+
id,
711+
window.kadenceCountdown.cache[id].parent
712+
);
713+
// Then set up interval
714+
window.kadenceCountdown.cache[id].interval = setInterval(function () {
715+
window.kadenceCountdown.updateTimerInterval(
716+
window.kadenceCountdown.cache[id].element,
717+
id,
718+
window.kadenceCountdown.cache[id].parent
719+
);
720+
}, 1000);
721+
}
722+
} else {
723+
// Pause
724+
window.kadenceCountdown.cache[id].paused = true;
725+
button.setAttribute('aria-pressed', 'true');
726+
button.setAttribute('aria-label', wp.i18n.__('Resume countdown timer', 'kadence-blocks'));
727+
button.setAttribute('title', wp.i18n.__('Resume countdown', 'kadence-blocks'));
728+
const icon = button.querySelector('.kb-countdown-pause-icon');
729+
if (icon) {
730+
icon.innerHTML =
731+
'<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M8 5v14l11-7z" fill="currentColor" /></svg>';
732+
}
733+
button.classList.add('kb-countdown-paused');
734+
735+
// Clear the interval
736+
if (window.kadenceCountdown.cache[id].interval) {
737+
clearInterval(window.kadenceCountdown.cache[id].interval);
738+
window.kadenceCountdown.cache[id].interval = null;
739+
}
740+
}
741+
},
635742
checkAndStartTimer(id, element, parent) {
636743
// Only check for evergreen timers
637744
if (window.kadenceCountdown.timers[id].type !== 'evergreen') {
638745
return;
639746
}
640747

748+
// Don't start if user prefers reduced motion
749+
if (window.kadenceCountdown.prefersReducedMotion) {
750+
// Just update once to show the initial time
751+
if (!window.kadenceCountdown.cache[id].started) {
752+
window.kadenceCountdown.cache[id].started = true;
753+
window.kadenceCountdown.updateTimerInterval(element, id, parent);
754+
}
755+
return;
756+
}
757+
758+
// Don't start if paused
759+
if (window.kadenceCountdown.cache[id] && window.kadenceCountdown.cache[id].paused) {
760+
return;
761+
}
762+
641763
// Check if timer should start based on viewport and active slide
642764
if (window.kadenceCountdown.isInViewport(parent) && window.kadenceCountdown.isInActiveSlide(parent)) {
643765
// Start the timer if it hasn't been started yet
@@ -671,6 +793,15 @@
671793
window.kadenceCountdown.cache[id].revealed = false;
672794
window.kadenceCountdown.cache[id].cookie = '';
673795
window.kadenceCountdown.cache[id].started = false;
796+
window.kadenceCountdown.cache[id].paused = false; // Start unpaused so initial display works
797+
window.kadenceCountdown.cache[id].element = element;
798+
window.kadenceCountdown.cache[id].parent = parent;
799+
800+
// Add accessibility role and live region to timer element.
801+
if (element) {
802+
element.setAttribute('role', 'timer');
803+
element.setAttribute('aria-live', 'polite');
804+
}
674805

675806
if (
676807
window.kadenceCountdown.timers[id].type === 'evergreen' &&
@@ -681,6 +812,51 @@
681812
);
682813
}
683814

815+
// Always update once to show the initial time (before setting paused state)
816+
window.kadenceCountdown.updateTimerInterval(element, id, parent);
817+
818+
// Setup pause button if it exists
819+
const pauseButton = parent.querySelector('.kb-countdown-pause-button');
820+
if (pauseButton) {
821+
// Remove any existing event listeners by cloning
822+
const newButton = pauseButton.cloneNode(true);
823+
pauseButton.parentNode.replaceChild(newButton, pauseButton);
824+
825+
// If prefers reduced motion, set button to play state initially and pause the timer
826+
if (window.kadenceCountdown.prefersReducedMotion) {
827+
window.kadenceCountdown.cache[id].paused = true;
828+
newButton.setAttribute('aria-pressed', 'true');
829+
newButton.setAttribute('aria-label', wp.i18n.__('Resume countdown timer', 'kadence-blocks'));
830+
newButton.setAttribute('title', wp.i18n.__('Resume countdown', 'kadence-blocks'));
831+
const icon = newButton.querySelector('.kb-countdown-pause-icon');
832+
if (icon) {
833+
icon.innerHTML =
834+
'<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M8 5v14l11-7z" fill="currentColor" /></svg>';
835+
}
836+
newButton.classList.add('kb-countdown-paused');
837+
}
838+
839+
// Add click handler
840+
newButton.addEventListener('click', (e) => {
841+
e.preventDefault();
842+
window.kadenceCountdown.togglePause(id, newButton);
843+
});
844+
845+
// Keyboard support
846+
newButton.addEventListener('keydown', (e) => {
847+
if (e.key === 'Enter' || e.key === ' ') {
848+
e.preventDefault();
849+
window.kadenceCountdown.togglePause(id, newButton);
850+
}
851+
});
852+
}
853+
854+
// Check if user prefers reduced motion - if so, don't start countdown interval
855+
if (window.kadenceCountdown.prefersReducedMotion) {
856+
// Timer is already displayed and paused, just don't start the interval
857+
return;
858+
}
859+
684860
// For evergreen timers, check viewport and active slide before starting
685861
if (window.kadenceCountdown.timers[id].type === 'evergreen') {
686862
// Setup scroll listener
@@ -716,6 +892,27 @@
716892
}
717893
},
718894
};
895+
// Listen for changes to prefers-reduced-motion
896+
if (window.matchMedia) {
897+
const motionQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
898+
const handleMotionChange = (e) => {
899+
const wasReduced = window.kadenceCountdown.prefersReducedMotion;
900+
window.kadenceCountdown.prefersReducedMotion = e.matches;
901+
902+
// If preference changed, restart all timers
903+
if (wasReduced !== e.matches) {
904+
// Reinitialize all timers
905+
window.kadenceCountdown.initTimer();
906+
}
907+
};
908+
909+
if (motionQuery.addEventListener) {
910+
motionQuery.addEventListener('change', handleMotionChange);
911+
} else {
912+
// Fallback for older browsers
913+
motionQuery.addListener(handleMotionChange);
914+
}
915+
}
719916
if ('loading' === document.readyState) {
720917
// The DOM has not yet been loaded.
721918
document.addEventListener('DOMContentLoaded', window.kadenceCountdown.initTimer);

src/blocks/countdown/block.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,14 @@
336336
"vsmobile": {
337337
"type": "boolean",
338338
"default": false
339+
},
340+
"enablePauseButton": {
341+
"type": "boolean",
342+
"default": false
343+
},
344+
"pauseButtonPosition": {
345+
"type": "string",
346+
"default": "top-right"
339347
}
340348
},
341349
"supports": {

0 commit comments

Comments
 (0)