|
6 | 6 | timers: JSON.parse(kadence_blocks_countdown.timers), |
7 | 7 | listenerCache: {}, |
8 | 8 | sliderEventCache: {}, |
| 9 | + prefersReducedMotion: window.matchMedia('(prefers-reduced-motion: reduce)').matches, |
9 | 10 | isInViewport(el) { |
10 | 11 | const rect = el.getBoundingClientRect(); |
11 | 12 | return ( |
|
168 | 169 | } |
169 | 170 | return element; |
170 | 171 | }, |
| 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 | + }, |
171 | 192 | updateTimerInterval(element, id, parent) { |
| 193 | + // Check if paused |
| 194 | + if (this.cache[id] && this.cache[id].paused) { |
| 195 | + return; |
| 196 | + } |
| 197 | + |
172 | 198 | const currentTimeStamp = new Date(); |
173 | 199 | const userTimezoneOffset = -1 * (new Date().getTimezoneOffset() / 60); |
174 | 200 | let total = ''; |
|
317 | 343 | window.location.href = window.kadenceCountdown.timers[id].redirect; |
318 | 344 | } |
319 | 345 | } 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 | + ); |
320 | 350 | parent.style.display = 'none'; |
321 | 351 | } else if ('message' === window.kadenceCountdown.timers[id].action) { |
322 | 352 | if (parent.querySelector('.kb-countdown-inner-first')) { |
|
328 | 358 | if (parent.querySelector('.kb-countdown-inner-second')) { |
329 | 359 | parent.querySelector('.kb-countdown-inner-second').style.display = 'none'; |
330 | 360 | } |
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 | + } |
333 | 381 | } |
334 | 382 | parent.style.opacity = 1; |
335 | 383 | if (window.kadenceCountdown.timers[id].revealOnLoad) { |
|
585 | 633 | if (sticky && !window.kadenceCountdown.timers[id].timer) { |
586 | 634 | setTimeout(function () { |
587 | 635 | 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 | + } |
589 | 639 | sticky.style.height = Math.floor(sticky.scrollHeight + parent.scrollHeight) + 'px'; |
590 | 640 | }, 200); |
591 | 641 | setTimeout(function () { |
|
632 | 682 | { element: slider, event: 'splide:moved', handler: movedHandler }, |
633 | 683 | ]; |
634 | 684 | }, |
| 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 | + }, |
635 | 742 | checkAndStartTimer(id, element, parent) { |
636 | 743 | // Only check for evergreen timers |
637 | 744 | if (window.kadenceCountdown.timers[id].type !== 'evergreen') { |
638 | 745 | return; |
639 | 746 | } |
640 | 747 |
|
| 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 | + |
641 | 763 | // Check if timer should start based on viewport and active slide |
642 | 764 | if (window.kadenceCountdown.isInViewport(parent) && window.kadenceCountdown.isInActiveSlide(parent)) { |
643 | 765 | // Start the timer if it hasn't been started yet |
|
671 | 793 | window.kadenceCountdown.cache[id].revealed = false; |
672 | 794 | window.kadenceCountdown.cache[id].cookie = ''; |
673 | 795 | 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 | + } |
674 | 805 |
|
675 | 806 | if ( |
676 | 807 | window.kadenceCountdown.timers[id].type === 'evergreen' && |
|
681 | 812 | ); |
682 | 813 | } |
683 | 814 |
|
| 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 | + |
684 | 860 | // For evergreen timers, check viewport and active slide before starting |
685 | 861 | if (window.kadenceCountdown.timers[id].type === 'evergreen') { |
686 | 862 | // Setup scroll listener |
|
716 | 892 | } |
717 | 893 | }, |
718 | 894 | }; |
| 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 | + } |
719 | 916 | if ('loading' === document.readyState) { |
720 | 917 | // The DOM has not yet been loaded. |
721 | 918 | document.addEventListener('DOMContentLoaded', window.kadenceCountdown.initTimer); |
|
0 commit comments