1/* ============================================================================ 2 * TFS 2991863 -- WCAG 3.2.5 link-opens-in-new-window announcement. 3 * Greystar (GS-22) fleet-wide runtime patch. 4 * 5 * Individually patching every widget/template that can emit target="_blank" 6 * hit a wall: a chunk of the offending links are not rendered by any 7 * ws-Kentico template or widget code at all -- they come from CMS-authored 8 * page-item content or Portal Engine-resolved macros that live in the 9 * CmsAdmin/Kentico DB, not this repo (see docs/reports/ 10 * 2991863-wcag-3.2.5-unassigned-zone-findings.html, "Ten Links, One Wall"). 11 * No source-controlled template fix can reach those. 12 * 13 * This script sidesteps the source-vs-content split by working on the 14 * RENDERED DOM instead of the markup source: whatever produced a 15 * target="_blank" link -- shared widget code, DB-stored content, a resolved 16 * macro -- the link still has to land in the DOM before a user or Silktide's 17 * crawler can see it. This scans for target="_blank" anchors missing a 18 * new-tab announcement and adds one, regardless of what put the link there. 19 * 20 * IDEMPOTENT BY DESIGN: several widgets already announce their own links 21 * (SocialMedia icons, Footer ADA/HUD logos, Header CTA getters -- all already 22 * emit "(opens in a new tab)" in an aria-label or a .sr-only span). This 23 * script must not double-announce those, so it checks the existing 24 * accessible name/text for that phrase first and skips if already present. 25 * 26 * FAST BY DESIGN: a single pass on DOM-ready, so Silktide's crawl (which 27 * waits for the page to settle) sees the mutated DOM. No polling, no 28 * MutationObserver -- content that loads after this pass runs is out of 29 * scope, same as it would be for any other first-paint accessibility check. 30 * 31 * Included ONLY on the GS-22 Greystar templates -- see 32 * Footer.ascx.cs:IsGreystarPriorityTemplate. Not a platform-wide change. 33 * ==========================================================================*/ 34 35(function ($) { 36 'use strict'; 37 38 if (!$) { 39 return; 40 } 41 42 var ANNOUNCED = /opens?\s+(?:in\s+)?(?:a\s+)?new\s+(?:tab|window)/i; 43 44 $(function () { 45 $('a[target="_blank"]').each(function () { 46 var $link = $(this); 47 48 var hiddenText = $link.find('.sr-only, .visually-hidden').text(); 49 var ariaLabel = $link.attr('aria-label') || ''; 50 var title = $link.attr('title') || ''; 51 var visibleText = $.trim($link.text()); 52 // Join every non-empty img[alt] in the link rather than just the 53 // first -- a decorative (empty-alt) icon ahead of a meaningful 54 // one must not shadow the meaningful one's name. 55 var imgAlt = $.trim($link.find('img[alt]').map(function () { 56 return $.trim($(this).attr('alt') || ''); 57 }).get().filter(function (a) { return a.length > 0; }).join(' ')); 58 59 if (ANNOUNCED.test(hiddenText) || ANNOUNCED.test(ariaLabel) || 60 ANNOUNCED.test(title) || ANNOUNCED.test(visibleText) || 61 ANNOUNCED.test(imgAlt)) { 62 return; 63 } 64 65 // Precedence follows the accessible-name computation: aria-label, 66 // then content (visible text or an img[alt] substitute), then 67 // title only as the last resort when there's no content at all. 68 if (ariaLabel) { 69 $link.attr('aria-label', ariaLabel + ' (opens in a new tab)'); 70 } else if (visibleText.length > 0) { 71 $link.append('<span class="sr-only"> (opens in a new tab)</span>'); 72 } else if (imgAlt.length > 0) { 73 $link.attr('aria-label', imgAlt + ' (opens in a new tab)'); 74 } else if (title) { 75 $link.attr('title', title + ' (opens in a new tab)'); 76 } else { 77 $link.attr('aria-label', 'Opens in a new tab'); 78 } 79 }); 80 }); 81})(window.jQuery);
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.