PageSourceSearch

https://quislingterrace.com/Assets/common/wcag/js/new-tab-announce.js

js quislingterrace.com collected 2026-09-26 16:39:12 UTC 4,068 bytes, 81 lines download raw bytes

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.