1/** 2 * Small theme-level accessibility enhancements for markup this theme does not 3 * emit directly (JS-mounted widgets + third-party plugin output). Each fix is a 4 * no-op when its target is absent, and re-runs on DOM mutations so it survives 5 * the player bundle's PJAX <main> swaps and async (Preact) mounts. 6 * 7 * 1. Name the program-archive date <select>. The <wmse-archive> widget (bundle- 8 * mounted on single-program.php) renders `Select Show Date<select>â¦` with a 9 * bare text node, not a real <label> â so the control has no accessible name 10 * (axe: select-name, critical). Add an aria-label. 11 * 2. Fix TEC Pro's series-meta list markup. On single events that belong to a 12 * series, Events Calendar Pro injects <dt>/<dd> as DIRECT children of 13 * <ul class="tribe-events-meta-list"> (axe: `list` â ul with non-li children; 14 * `dlitem` â dt/dd outside a <dl>). Wrap those stray <dt>/<dd> in <li><dl>. 15 * No supported PHP template override exists for this custom-tables output. 16 * 3. Reveal Gravity Forms wrappers left inline-hidden by a PJAX swap (GF's own 17 * un-hide never takes effect there â see the note on the function itself). 18 * 4. Restore keyboard access to the /events/ contest CTA â TEC's mobile 19 * focus-order pass stamps tabindex="-1" on it below ~700px. 20 * 5. Hero-slider a11y: make the chevrons keyboard-operable (the bundle's controls are 21 * role-less <div>s bound on `mousedown`, so they were mouse-only), and announce 22 * slide changes through a live region â user-initiated changes only, never the 23 * 7s auto-advance. 24 */ 25(function () { 26 function nameArchiveSelects(root) { 27 var scope = root && root.querySelectorAll ? root : document; 28 var sels = scope.querySelectorAll('wmse-archive select, .the-show--actions-row select'); 29 for (var i = 0; i < sels.length; i++) { 30 if (!sels[i].getAttribute('aria-label')) { 31 sels[i].setAttribute('aria-label', 'Select show date'); 32 } 33 } 34 } 35 36 function fixSeriesMetaList(root) { 37 var scope = root && root.querySelectorAll ? root : document; 38 var lists = scope.querySelectorAll('ul.tribe-events-meta-list'); 39 for (var i = 0; i < lists.length; i++) { 40 var ul = lists[i]; 41 // stray <dt>/<dd> that are DIRECT children of the <ul> 42 var stray = []; 43 for (var c = 0; c < ul.children.length; c++) { 44 var tag = ul.children[c].tagName; 45 if (tag === 'DT' || tag === 'DD') stray.push(ul.children[c]); 46 } 47 if (!stray.length) continue; 48 var li = document.createElement('li'); 49 li.className = 'wmse-meta-series'; 50 var dl = document.createElement('dl'); 51 dl.className = 'wmse-meta-series__dl'; 52 // insert the <li> where the first stray element was, then move them in 53 ul.insertBefore(li, stray[0]); 54 li.appendChild(dl); 55 for (var s = 0; s < stray.length; s++) dl.appendChild(stray[s]); 56 } 57 } 58 59 /** 60 * 3. Reveal Gravity Forms stranded by PJAX (2026-08-03, Marty: "the 61 * volunteer application never shows up unless you hard refresh"). 62 * GF's ajax mode renders `.gform_wrapper` with an inline 63 * display:none (anti-flicker) and un-hides it from an INLINE script. 64 * â CORRECTED 2026-08-05b: that script is NOT skipped â inline scripts 65 * DO execute in a PJAX-swapped <main> (CLAUDE.md landmine 12). It runs 66 * and THROWS, because GF's JS dependencies aren't initialised on the 67 * swapped page â `gform.addAction is not a function` sits in 68 * module-sandbox-verify's reported page errors. Either way the form 69 * arrives in the markup and stays invisible, and this fix is unchanged: 70 * GF's reveal runs at DOMContentLoaded on hard loads; anything still 71 * hidden ~400ms after nodes arrive is stuck â show it. (Field-level conditional-logic 72 * hiding lives on .gfield, untouched here.) The forms MODULE renders 73 * non-ajax and never hides; this covers the legacy content-embedded 74 * forms (12 pages). 75 */ 76 function revealStuckGforms() { 77 var wraps = document.querySelectorAll('.gform_wrapper'); 78 for (var i = 0; i < wraps.length; i++) { 79 if (wraps[i].style.display === 'none') { 80 wraps[i].style.display = ''; 81 } 82 } 83 } 84 85 var gfTimer = null; 86 function scheduleGformReveal() { 87 if (gfTimer) clearTimeout(gfTimer); 88 gfTimer = setTimeout(revealStuckGforms, 400); 89 } 90 91 /** 92 * FIX #4 â the /events/ contest CTA is not keyboard-reachable below ~700px. 93 * 94 * TEC's mobile focus-order script walks the archive header and stamps 95 * tabindex="-1" + data-tribe-mobile-focus-order="skipped" onto elements it 96 * decides are decorative â including our "Contest Rules and Entry" button, 97 * which is the only call to action in that header. Mouse users are fine (this 98 * is NOT the homepage stacking bug, which is a CSS fix in 99 * _tribe-events-v2.scss); keyboard and screen-reader users lose it entirely. 100 * 101 * We can't stop TEC stamping it, so we un-stamp it â and because the observer 102 * re-runs run(), a re-stamp after an AJAX view swap gets corrected too. Only 103 * OUR button is touched: TEC's own skip decisions are left alone. 104 */ 105 function restoreEventsCtaFocus(root) { 106 var ctas = (root || document).querySelectorAll('.wmse-events-cta[tabindex="-1"]'); 107 for (var i = 0; i < ctas.length; i++) { 108 ctas[i].removeAttribute('tabindex'); 109 ctas[i].removeAttribute('data-tribe-mobile-focus-order'); 110 } 111 } 112
113 /** 114 * FIX #5 â the hero-slider chevrons are mouse-only. 115 * 116 * The bundle renders/consumes bare `<div x-wmse-next-control>` / 117 * `<div x-wmse-previous-control>` with no tabindex, no role and no accessible 118 * name, and it binds them on **`mousedown`** â not click: 119 * 120 * this.$previousControl.on("mousedown", this.gotoPrevious.bind(this)); 121 * this.$nextControl.on("mousedown", this.gotoNext.bind(this)); 122 * 123 * So a keyboard user could not advance the carousel at all. (axe doesn't flag it 124 * â a role-less <div> isn't expected to be operable â which is why it sat 125 * unnoticed.) Content was never unreachable: it auto-advances every 7s and each 126 * slide's link is individually tab-reachable. But the controls were not. 127 * 128 * TWO SHAPES TO SUPPORT, deliberately: 129 * ⢠`module-hero_slider.php` emits real <button>s â semantics where we own the 130 * markup. 131 * ⢠`page_home.php`'s static still emits <div>s and is OUT OF SCOPE (owner 132 * decision), and the shape is DO-NOT-TOUCH anyway. So we upgrade it in place 133 * here: role="button" + tabindex="0" + a name. Same fix reaches both, and the 134 * home page gets it without editing a guardrailed template. 135 * 136 * Keyboard activation is bridged to the synthetic `mousedown` the bundle listens 137 * for â exactly the technique js/wmse-slider-gestures.js already uses for swipes. 138 */ 139 function enhanceSliderControls(root) { 140 var scope = root && root.querySelectorAll ? root : document; 141 var controls = scope.querySelectorAll('[x-wmse-next-control], [x-wmse-previous-control]'); 142 for (var i = 0; i < controls.length; i++) { 143 var el = controls[i]; 144 if ('BUTTON' !== el.tagName) { 145 // the legacy <div> shape â give it button semantics in place 146 if (!el.hasAttribute('role')) { el.setAttribute('role', 'button'); } 147 if (!el.hasAttribute('tabindex')) { el.setAttribute('tabindex', '0'); } 148 } 149 if (!el.getAttribute('aria-label')) { 150 el.setAttribute('aria-label', 151 el.hasAttribute('x-wmse-next-control') ? 'Next slide' : 'Previous slide'); 152 } 153 } 154 155 // Create each slider's live region EAGERLY, not on the first announcement: a 156 // live region has to be in the DOM before content is written into it, or some 157 // assistive tech misses the first change entirely. 158 var sliders = scope.querySelectorAll('wmse-slider'); 159 for (var s = 0; s < sliders.length; s++) { 160 sliderStatusRegion(sliders[s]); 161 } 162 } 163 164 var SLIDER_CONTROLS = '[x-wmse-next-control], [x-wmse-previous-control]'; 165 166 function sliderControlFrom(target) { 167 return target && target.closest ? target.closest(SLIDER_CONTROLS) : null; 168 } 169 170 function advanceSlider(control) { 171 control.dispatchEvent(new MouseEvent('mousedown', { 172 bubbles: true, 173 cancelable: true, 174 view: window, 175 })); 176 } 177 178 /** 179 * SLIDE-CHANGE ANNOUNCEMENTS (part of fix #5). 180 * 181 * The carousel gave assistive tech no signal that the slide had changed â pressing 182 * the chevron did something visible and announced nothing. The bundle emits no 183 * events and keeps no reachable instance, so this is done from the outside: a 184 * visually-hidden live region per slider, written after the transition settles. 185 * 186 * â USER-INITIATED CHANGES ONLY. The 7s auto-advance is deliberately SILENT â 187 * announcing every rotation would interrupt a screen-reader user roughly nine times 188 * a minute, which is worse than saying nothing (the APG carousel pattern keeps 189 * auto-rotation out of the live region for exactly this reason). 190 * 191 * The distinction is free: mouse, keyboard (via the bridge above) and swipe (via 192 * js/wmse-slider-gestures.js) ALL funnel through a `mousedown` on a control, and 193 * the auto-advance does not. So listening for that one event is precisely the set 194 * of changes worth announcing. 195 */ 196 var statusSeq = 0; 197 198 function sliderStatusRegion(slider) { 199 var id = slider.getAttribute('data-wmse-status-id'); 200 var region = id ? document.getElementById(id) : null; 201 if (region) { return region; } 202 203 statusSeq++; 204 id = 'wmse-slider-status-' + statusSeq; 205 region = document.createElement('div'); 206 region.id = id; 207 region.className = 'screen-reader-text'; 208 region.setAttribute('role', 'status'); 209 region.setAttribute('aria-live', 'polite'); 210 region.setAttribute('aria-atomic', 'true'); 211 // OUTSIDE <wmse-slider> on purpose: the bundle hands its childNodes to Preact 212 // at mount, so keeping our node out of that subtree avoids any interaction. 213 if (slider.parentNode) { 214 slider.parentNode.insertBefore(region, slider.nextSibling); 215 } 216 slider.setAttribute('data-wmse-status-id', id); 217 return region; 218 } 219 220 function activeSlideIndex(slider) { 221 var pics = slider.querySelectorAll('picture'); 222 for (var i = 0; i < pics.length; i++) { 223 var t = pics[i].style.transform || ''; 224 if (t.indexOf('translate(0%') !== -1 || t.indexOf('translate(0px') !== -1) { 225 return i; 226 } 227 } 228 return -1; 229 } 230 231 function announceSlide(slider, i) { 232 var pics = slider.querySelectorAll('picture'); 233 var img = pics[i] ? pics[i].querySelector('img') : null; 234 var name = img ? (img.getAttribute('alt') || '').trim() : ''; 235 sliderStatusRegion(slider).textContent = 236 'Slide ' + (i + 1) + ' of ' + pics.length + (name ? ': ' + name : ''); 237 } 238 239 /** 240 * Announce once the slide has ACTUALLY changed. 241 * 242 * â Must poll for a change from `fromIndex`, not merely for "a settled index". 243 * The listener below runs on the CAPTURE phase, i.e. before the bundle's own 244 * bubble-phase handler, so an immediate read returns the slide you are LEAVING â 245 * which announced everything one behind (caught in testing: pressing next on 246 * slide 1 announced "Slide 1 of 3"). Home masked it by racing the auto-advance. 247 * 248 * If the index never changes, the bundle's `isAnimating` guard swallowed the press 249 * â correctly announce nothing. 250 */ 251 function scheduleAnnounce(slider, fromIndex) { 252 var pics = slider.querySelectorAll('picture'); 253 if (pics.length < 2) { return; } // one slide can't change 254 var tries = 0; 255 (function poll() { 256 tries++; 257 var now = activeSlideIndex(slider); 258 if (now !== -1 && now !== fromIndex) { 259 slider.setAttribute('data-wmse-status-index', String(now)); 260 announceSlide(slider, now); 261 return; 262 } 263 if (tries < 14) { setTimeout(poll, 120); } // ~1.7s; the animation is 250ms 264 }()); 265 } 266 267 // The single funnel: every USER-initiated change performs or dispatches a
268 // `mousedown` on a control â mouse natively, keyboard via the bridge above, swipe 269 // via js/wmse-slider-gestures.js. The 7s auto-advance never does, which is exactly 270 // why it stays out of the live region. 271 document.addEventListener('mousedown', function (e) { 272 var c = sliderControlFrom(e.target); 273 if (!c) { return; } 274 var slider = c.closest ? c.closest('wmse-slider') : null; 275 if (!slider) { return; } 276 // read the CURRENT slide synchronously, before the bundle moves it 277 scheduleAnnounce(slider, activeSlideIndex(slider)); 278 }, true); 279 280 // Bound ONCE, delegated on document â so it covers the static, the module, any 281 // number of sliders, and anything arriving via a PJAX swap, with no re-init. 282 document.addEventListener('keydown', function (e) { 283 if ('Enter' !== e.key && ' ' !== e.key && 'Spacebar' !== e.key) { return; } 284 var c = sliderControlFrom(e.target); 285 // a real <button> emits its own click for Enter/Space â handled below, so 286 // don't also fire here or the slider would advance twice 287 if (!c || 'BUTTON' === c.tagName) { return; } 288 e.preventDefault(); // Space would otherwise scroll the page 289 advanceSlider(c); 290 }); 291 292 document.addEventListener('click', function (e) {
293 // detail === 0 means KEYBOARD activation. A real mouse click has detail >= 1 294 // and the bundle's own mousedown handler has already advanced the slider â 295 // bridging that too would double-advance. 296 if (0 !== e.detail) { return; } 297 var c = sliderControlFrom(e.target); 298 if (!c || 'BUTTON' !== c.tagName) { return; } 299 e.preventDefault(); 300 advanceSlider(c); 301 }); 302 303 function run(root) { 304 nameArchiveSelects(root); 305 fixSeriesMetaList(root); 306 restoreEventsCtaFocus(root); 307 enhanceSliderControls(root); 308 scheduleGformReveal(); 309 } 310 311 if (document.readyState === 'loading') { 312 document.addEventListener('DOMContentLoaded', function () { run(document); }); 313 } else { 314 run(document); 315 } 316 317 // Re-run when new nodes arrive (async widget mounts + PJAX <main> swaps). 318 if (window.MutationObserver && document.body) { 319 var scheduled = false; 320 var mo = new MutationObserver(function (muts) { 321 for (var i = 0; i < muts.length; i++) { 322 if (muts[i].addedNodes && muts[i].addedNodes.length) { 323 if (!scheduled) { 324 scheduled = true; 325 (window.requestAnimationFrame || window.setTimeout)(function () { 326 scheduled = false; 327 run(document); 328 }); 329 } 330 break; 331 } 332 } 333 }); 334 mo.observe(document.body, { childList: true, subtree: true }); 335 } 336})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.