Skip to main content

Career Site Accessibility Patch v2.0.4

This patch addresses various accessibility issues across all Career Sites. This is a temporary solution until these issues can be resolved by the product team. If there are any questions or concerns with the script, please reach out to Michael Spellacy

Installation Anchor ()

Before installing MagicBullet, please verify that it is not already running. If it is not, add the following script:

<script id="radancy-magicbullet" data-a11y src="https://services.tmpwebeng.com/magicbullet/" type="module"></script>

Where To Install

Never install MagicBullet at the theme level, as doing so can cause script conflicts and complicate future maintenance.

Verification Anchor ()

There are two ways to verify that this script is running:

If these messages are missing or you are receiving an alert in A11y Pulse, the patch isn't loading and you'll need to troubleshoot.

Troubleshooting Anchor ()

  1. Ensure only one instance of MagicBullet loads per page. Multiple MagicBullet scripts can prevent associated features, like the Accessibility Patch, from running.
  2. MagicBullet can also be initiated via JavaScript to support conditional loading (usually of data attributes). If a conflict occurs, check whether the script is created dynamically. If it exists in your JavaScript, you may be able to remove it and copy the attributes over to the inline version or add the data-a11y attribute here instead. It may look like this:

    var magicBullet = document.createElement("script");
    magicBullet.setAttribute("id", "radancy-magicbullet");
    magicBullet.setAttribute("data-gdpr", "false");
    magicBullet.setAttribute("data-gdpr-client-name", "Acme Inc.");
    magicBullet.setAttribute("data-gdpr-policy-url", "https://acme.com/privacy-policy.html");
    magicBullet.setAttribute("src", "https://services.tmpwebeng.com/magicbullet/");
    document.body.appendChild(magicBullet);

  3. Razor commenting does not appear to be supported in Settings. If an older version of MagicBullet exists there, delete it entirely or use HTML comments to remove it.
  4. Alternatively, if you beleive that MagicBullet is causing an issue on your page, you may temporarily disabled it by passing the following URL paramater: ?magicbullet=false.

Post-Installation Smoke-Test Anchor ()

After it has been confirmed that the script is running you must smoke-test your pages to ensure the site's core functionality still works and that there are no visual issues. MagicBullet only addresses issues under the hood that relate to accessibility, but minor issues can occasionally occur. If you are sure that MagicBullet is causing an issue, please reach out to Michael Spellacy.

Issue Log Anchor ()

Images Missing Alt Attributes Anchor ()

Priority
High
Product
Career Site (Client Facing)
Associated Module
Global
Associated Function
fixAltAttribute
Description

Our delivery developers and content authors, despite our best efforts to educate them, often forget to include an alt attribute on images. Adding an alt attribute, often called a “null alt”, to decorative or non-essential images will effectively hide them from assistive technology, creating a more pleasant and less overwhelming experience. Missing alt attributes are also cited in internal and third-party accessibility audits, so by automating their inclusion, we eliminate both of these issues.

Fixes
  1. Look for images on page that do not contain an alt attribute and add one dynamically.

Applied Filters Require Several UX/AX Improvements Anchor ()

Priority
High
Product
Career Site (Client Facing)
Associated Module
Search Results
Associated Function
fixAppliedFilter
Language Support
Required
Description

Although a part of the Search Results module, the applied filter buttons at the top of the Search Results page have several issues that are being addressed with this fix. These buttons appear after certain filters are selected or specialized URLs are navigated to. The improvements include more intuitive accessible labels that include and better keyboard management. For example, when a filter is removed, the focus moves to the next adjacent button. If the last filter is removed, the focus shifts to the first search result. The fix also makes several improvements and adjustments to the ARIA used in this section. Note: May eventually merge this with fixSearchResults to keep related module issues together.

Fixes
  1. Remove aria-labeledby from UL
  2. Remove aria-hidden and aria-expanded and add aria-describedby and role to parent element (.search-results-option)
  3. Add aria-label to each button that is not disabled. Include a better label that helps identify the functionality of the button.

Aria-describedby Pointing to Non-Existent IDs Anchor ()

Priority
Medium
Product
Career Site (Client Facing)
Associated Module
Cookie Management
Associated Function
fixCookieManagement
Description

Input elements on the Cookie Management page currently include aria-describedby attributes that reference non-existent IDs. If the ID is created later, such as post-validation, the aria-describedby attribute should only be present at that state. For now, we are simply removing this attribute, which may also be the recommended fix for the product team.

Fixes
  1. Remove aria-describedby=”cookieDescriptionIdAttr” from each input element.

Data Collection Forms (Job Alert, Talent Community) Require (Many) UX/AX Improvements Anchor ()

Priority
High
Product
Career Site (Client Facing)
Associated Module
Form (Job Alert)
Associated Function
fixDataForm
Language Support
Required
Description

Many of our data capture forms on the site, such as Job Alerts, have dozens of problems that impact users with disabilities. This fix will address all forms that currently have a class of “data-form”. These fixes range in nature from addressing improper lableing, to improving validation, general clean-up, and more.

Fixes
  1. Remove the CSS asterisk (see init.scss) because it reads out to assistive tech (AT) when added via content property and include a span with aria-hidden on it so that it is not picked up by AT. New asterisk applied via CSS.
  2. Remove “role” from field-validation-error (it’s not needed).
  3. All required fields should include aria-invalid=”false” on page load.
  4. The input-validation-error class is removed when leaving a field, but aria-invalid remains set to true, so we address this by listening for blur event.
  5. It is customary to include more useful autocomplete attributes so that certain fields will show the contextual menu for easier input. Adding autocompletes for First Name, Family Name, and Email. For email we are also changing the type from “text” to “email”
  6. The “Add” button needs to be more explicit to AT so include “Add Job Alert”
  7. The keyword list requires a more accessible grouping to better identify it; adding proper role and accName, etc.
  8. The file upload remove button is a link with an href hash. This is awful, so let’s remedy it by replacing it with a button.
  9. Adding an accName to textarea, removing iframe garbage, and moving captcha to end of form to address tab order. It should not exist before submit button.
  10. The “Sign Up” button needs to be more explicit to AT.
  11. The form message has an inline tabindex=”0” on it. This is not ideal as messages that receive focus should only do so temporarily and not when user tabs back to it.
  12. The form message close link should really be a button, but for now we’ll simply add a role.
  13. The Keyword Location field does not appear to have an aria-describedby on it when an error is returned, so we need to grab it from Keyword Category and dupe it here.
  14. Now that we are including aria-invalid, we need to alter the values based on user input.

Core Disclosure Functionality Placing aria-expanded on Incorrect Element Anchor ()

Priority
High
Product
Career Site (Client Facing)
Associated Module
Global
Associated Function
fixGlobalDisclosure
Description

Some areas of our career sites utilize a class called “expandable-parent” that incorrectly applies the aria-expanded attribute to itself—the div or section to which the class is applied—instead of to the interactive element, typically a button, where it should actually exist. The aria-expanded attribute only conveys the state of the component (expanded or collapsed) when it is present on the interactive element that toggles it and not its parent element.

Currently, this default functionality can be found in the Seach Filters, though it may appear elsewhere.

Fixes
  1. See if element is already open, set aria-expanded state to true if it is.
  2. Remove aria-expanded from adjacent, non-interactive element.
  3. New toggle functionality for newly added aria-expanded attribute.
  4. Remove aria-expanded being added to adjacent, non-interactive element, by CS Core.

Iframe Element Improvements Anchor ()

Priority
Low
Product
Career Site (Client Facing)
Associated Module
Global
Associated Function
fixIframeElement
Language Support
Required
Description

Like images, iframes often require additional information to convey a description of the content they contain. Ideally, this information would always be included, but until then, we can apply a fix to improve the situation. For example, YouTube videos, especially those in CMS-generated content, often lack a title attribute, so this fix will address that specifc issue.

Fixes
  1. Find YouTube embeds that may be missing the title attribute and add a generic one.

Input Element Improvements Anchor ()

Priority
Low
Product
Career Site (Client Facing)
Associated Module
Global
Associated Function
fixInputElements
Description

Many input elements that are checkboxes also contain an attribute called autocomplete. Since autocompete invalidates on these kinds of inputs, we need to remove the attribute. This fix may also address other kinds of common input issues in the future.

Fixes
  1. Input elements with type of “checkbox” should not contain autocomplete as it is not a text or select field, so we need to remove.

Job Description Improvements Anchor ()

Priority
High
Product
Career Site (Client Facing)
Associated Module
Job Details
Associated Function
fixJobDescription
Description

CMS-generated content can produce a lot of access barriers and broken code, so this fix seeks to address several common issues found in ATS job descriptions.

Fixes
  1. Remove tabindex attribute from elements within .ats-description that have tabindex attribute not equal to “0” or starting with “-“.
  2. Add role=”presentation” to tables within .ats-description
  3. Remove useless attributes from all elements within .ats-description
  4. Remove element and unwrap its contents within .ats-description

Job List Improvements Anchor ()

Priority
Medium
Product
Career Site (Client Facing)
Associated Module
Job List
Associated Function
fixJobList
Description

Job Lists links should have the associated location appear as a child text node within them, so that each link can be more descriptive and discernable from other jobs within the list that may contain the same job title.

Fixes
  1. Move location in into Job List link.

Job Location Page Improvements Anchor ()

Priority
High
Product
Career Site (Client Facing)
Associated Module
Job Location Map
Associated Function
fixJobLocation
Description

The form on the Job Location page has several issues that this fix addresses. Keyboard users find the map difficult to navigate. To address this, it has been suggested to provide a link to access the equivalent map on Google.com, which is more accessible for keyboard and assistive technology users. Additionally, the primary heading, “Job Location,” should specify the exact location being sought.

Fixes
  1. Job map links should really be buttons, not links. Including role=”button” for now, but we need to add spacebar key support eventually.
  2. Links should never open in new windows without an exceptional reason, so removing target attribute.

Save Job Button Improvements Anchor ()

Priority
High
Product
Career Site (Client Facing)
Associated Module
Global
Associated Function
fixSaveJobButton
Language Support
Required
Description

The “Save Job” button used across several components are not accessible. When activated, the button needs to convey a “pressed” state to assistive technologies, indicating whether the button has been activated or not. The button should consistently display “Save Job” to ensure clarity when used together with aria-pressed. Using text to toggle between “Save Job” and “Job Saved” is ineffective and wrought with accessibility issues. Therefore, when aria-label is applied, which is also scripted in. it should take precedence over the child text being toggled and displayed. On this last point, hearing one thing and seeing another may still fail WCAG but the experience is still much improved. Develoeprs and designers should be incouraged not to toggle text to convey state.

Fixes
  1. Custom label needed to override the text toggle that delivery often adds (or removes). Text changes should never be used to convey state.
  2. iOS, NVDA bug, state not reading back so need to implicitly add a role of “button” and remove type attribute. Do not remove until support better.
  3. aria-pressed required, which will properly convey state of the button.

Basic Search Form Improvements Anchor ()

Priority
High
Product
Career Site (Client Facing)
Associated Module
Search Form
Associated Function
fixSearchForm
Description

The basic Search Form has several issues, particularly with the validation of the “Locations” field in how it relates to keyboard use. When an invalid location is entered, focus should return to the Location field instead of being placed on the error itself. This ensures that screen reader users hear the error message beneath it and can tab forward after correcting the error, rather than tabbing backwards if focus is on the error message itself. Additionally, the aria-hidden attribute should be removed from the “Location Pin” button. Currently, this fix is only applied to the basic search form. A new function will eventually be created for the Advanced Search Form, which has similar issues.

Fixes
  1. Our primary search form should include a distinct role so that it can aid in helping assistive technology users navigate the page, so adding role=”search”
  2. Our primary search form should provide a better group description for the fields at hand. This can be achieved by adding a div with a class of “job-search-legend” (a heading should not be used) and the following script will take care of adding any needed associations by either adding aria-describedby or aria-label, with appropriate IDs.
  3. Remove aria-expanded from .search-form-fields
  4. The way in which the validation is currently handled requires improvement to aid those using assistive technology and keyboard. Here we need to include aria-invalid and aria-describedby to the locations field and apply custom validation as well, so that keyboard users do not need to tab backwards to return to the locations field, which is initiating the error.
  5. Apply a unique ID to the error message. This is what locations field will read when focus is brought back to it, thanks to aria-describedby.
  6. When search button is pressed, fire off custom validation.
  7. Remove aria-hidden from the location pin. Often this is visually displayed, but aria-hidden is still present.

Search Filter Improvements Anchor ()

Priority
High
Product
Career Site (Client Facing)
Associated Module
Search Filters
Associated Function
fixSearchResults
Description

The Search Filters section requires some improvments to help aid in the understanding of the controls. For example, checkboxes should always be properly grouped, for starters.

Fixes
  1. Add role and accName to Search Filter section.
  2. Each section element (.expandable) should contain a role of “group” as well as an accName. We will need to add an ID to each button for aria-labelledby.

Search Results Section Missing Tabindex Anchor ()

Priority
Medium
Product
Career Site (Client Facing)
Associated Module
Search Results
Associated Function
fixSearchResults
Description

The Search Results section appears to be missing a tabindex, so this fix will add one.

Fixes
  1. When tabindex 0 was removed, visible focus is now lost. Product team should be applying tabindex -1 in addition to focus.

Search Results Pagination Improvements Anchor ()

Priority
Medium
Product
Career Site (Client Facing)
Associated Module
Search Results
Associated Function
fixSearchPagination
Language Support
Required
Description

The Search Results pagination has several issues that need to be addressed. Since the pagination is enclosed in a nav element, it should be properly labeled. This is important if other nav elements exist on the page. Additionally, the “Next” and “Previous” links should be hidden on the first and last pages to avoid any confusion, as they lead nowhere and are improperly hidden from assistive technology. Note: May eventually merge this with fixSearchResults to keep related module issues together.

Fixes
  1. Pagination(s) in Search Results should really have an accName so it can be differentiated between other nav elements that may exist on page.
  2. Search Results pagination disabled button can be tabbed to (this is bad). To address this, we simply remove href. When removed, aria-hidden is not really needed, so we remove that, too!

Job Location and Job Category Module Headings Contain Tabindexes Anchor ()

Priority
Medium
Product
Career Site (Client Facing)
Associated Module
Job Location
Associated Function
fixSitemap
Description

The Sitemap page contains certain headings that include tabindex and aria-expanded attributes, as well as the .expandable-parent class. Non-interactive elements should never contain these. When we do have need to include interactive components in our product, we should be using elements like button. This issues appears to impact modules like “Job Location” and “Job Category,” which are commonly found on the Sitemap page.

TODO: If this function is still required, consider breaking out into new functions to reflect specific modules they are associated with, rather than the Sitemap page.

Fixes
  1. Remove tabindex, aria-expanded, and .expandable-parent from .job-location h2 and .job-category h2 element.

Social Share Links That Open In New Windows Require Helper Text Anchor ()

Priority
High
Product
Career Site (Client Facing)
Associated Module
Social Share
Associated Function
fixSocialShare
Language Support
Required
Description

The Social Media Share links on most job description pages open in new windows. We need to include helper text to inform assistive technology users of this behavior.

Fixes
  1. Append the following element and helper text, <span class="wai visually-hidden">(Opens in new tab)</span>, to each .social-share-items a element.
/*!

  Radancy MagicBullet: Accessibility Patch

  Contributor(s):
  Michael "Spell" Spellacy

*/

/* TODO

1. Add new fix for keyword autocomplete. See https://dwhite-rds.runmytests.com/documentation#section-auto-complete 
   Similar was implemented on Intuit some time ago. Will probably not use, except for refrence. See https://tbadmin.radancy.net/sassjs/updatejavascript/27595/27595/70305

*/ 

function loadA11yPatch(url, callback) {

  var getComponentLanguagePack = document.getElementById("magicbullet-language-pack");

  if (getComponentLanguagePack) {

    callback();

  } else {

    // Install Language Pack.

    var componentLanguagePack = document.createElement("script");

    componentLanguagePack.setAttribute("src", url);
    componentLanguagePack.setAttribute("id", "magicbullet-language-pack");
    componentLanguagePack.onreadystatechange = callback;
    componentLanguagePack.onload = callback;

    document.getElementsByTagName("head")[0].appendChild(componentLanguagePack);

  }

}

var languagePackSrc = "https://services.tmpwebeng.com/magicbullet/language-pack.js";

loadA11yPatch(languagePackSrc, function(){

  console.log("%cMagicBullet: A11y%cv2.0.4", "background: #2d2d2d; color: #fff; padding: 6px 10px; border-radius: 16px 0 0 16px; font-weight: 600;" , "background: #6e00ee; color: #fff; padding: 6px 10px; border-radius: 0 16px 16px 0; font-weight: 600;");

  var magicBulletScript = document.getElementById("tmp-magic-bullet") ? document.getElementById("tmp-magic-bullet") : document.getElementById("radancy-magicbullet");

  var a11yBody = document.body;

  // Add A11y hook for implementation team. May come in handy.

  a11yBody.classList.add("magicbullet-a11y");

  // Add global aria-live to pass messages to when needed.

  let globalA11yMessage = document.querySelector("#magicbullet-message");

  if(!globalA11yMessage) {

    let a11yMessage = document.createElement("div");
  
    a11yMessage.classList.add("magicbullet-visually-hidden");
    a11yMessage.setAttribute("id", "magicbullet-message");
    a11yMessage.setAttribute("aria-live", "polite");
    a11yBody.append(a11yMessage);

  }

  // Create a new MutationObserver instance

  // TODO: Instead of having an observer to watch _everything_ in main (#content), consider setting up multiple observers per component.
  // This might be better and less prone to creating interference on site. Similar to MindReaders seperate observer.

  var targetNode = document.querySelector("#content");

  // TODO: Rather than observe everything in main, only observe certain components on page that may be impacted. 
  
  var config = { childList: true, subtree: true };
  
  var a11yObserver = new MutationObserver(function(mutationsList) {

    // Log mutations
  
    // console.log("Mutations:", mutationsList);
    
    clearTimeout(a11yObserver.timeout);
  
    a11yObserver.disconnect();
    
    a11yObserver.timeout = setTimeout(function() {
  
      initDynamicPatch();
  
      a11yObserver.observe(targetNode, config); 
    
    }, 1000);
    
  });
  
  a11yObserver.observe(targetNode, config);

  initStaticPatch();
  
  initDynamicPatch(); 

  // MindReader Observers 

  var mindReaderNodes = document.querySelectorAll(".mindreader-status, #wrapper-sel-city");

  function callback(mutationList, mindReaderObserver) {
 
    fixMindReaderList();

  }

  mindReaderNodes.forEach(function(node) {
  
    var mindReaderObserver = new MutationObserver(callback);
    
    mindReaderObserver.observe(node, config);

  });

});

// Accessibility Patch: Dynamic
// Desc: These fixes address issues that occur both on page load and during dynamic DOM changes. 
// For instance, a script might add a new node to the DOM or refresh content on the page. When this happens, each fix will be reapplied to DOM.
  
function initDynamicPatch() {

  fixAltAttribute();
  fixAppliedFilter();
  fixDataForm();
  fixGlobalDisclosure();
  fixIframeElement();
  fixInputElements();
  fixMindReaderInput();
  fixSaveJobButton();
  fixSearchFilters();
  fixSearchResults();
  fixSearchPagination();

  console.log("%cMagicBullet: A11y%cDynamic Scripts \u2714", "background: #2d2d2d; color: #fff; padding: 6px 10px; border-radius: 16px 0 0 16px; font-weight: 600;" , "background: #228B22; color: #fff; padding: 6px 10px; border-radius: 0 16px 16px 0; font-weight: 600;");

}

// Accessibility Patch: Static
// Desc: These fixes address issues that occur on page load only.

function initStaticPatch() {

  fixA11y();
  fixAdvancedSearchForm();
  fixCookieManagement();
  fixCMS();
  fixHeader();
  fixJobDescription();
  // fixJobList();
  fixJobLocation(); 
  fixJobMap();
  fixSearchForm();
  fixSitemap();
  fixSocialShare();

  console.log("%cMagicBullet: A11y%cStatic Scripts \u2714", "background: #2d2d2d; color: #fff; padding: 6px 10px; border-radius: 16px 0 0 16px; font-weight: 600;" , "background: #228B22; color: #fff; padding: 6px 10px; border-radius: 0 16px 16px 0; font-weight: 600;");

}

// Accessibility Patch: Fix A11y
// Description: This is a global function that addresses sitewide accessibility.

function fixA11y() {

  // Add Tabindex to main for max support. 

  const mainContent = document.querySelector("#content");

  if (mainContent) {
  
    // mainContent.setAttribute("tabindex", "-1"); removing for now because the search filters appear to break when present. 
    // roll an eventual fix on this that removes the 
  
  }

}

// Accessibility Patch: Advanced Search Form 

function fixAdvancedSearchForm() {

  var searchForm = document.querySelectorAll(".advanced-search-form");

  searchForm.forEach(function(form, i){

    var formID = (i + 1); // Not used (yet).
    var searchFormFields = form.querySelector(".advanced-search-form-fields");

    if(searchFormFields) {

      // Fix: Remove aria-expanded from .advanced-search-form-fields.

      searchFormFields.removeAttribute("aria-expanded");

    }

  });

}

// Accessibility Patch: Alt Attribute (Utility)

function fixAltAttribute() {

  // Fix: Look for images on page that do not contain an alt attribute and add one dynamically.
  // Fix: Alt text with spaces in them are not ideal and even get picked up by certain linters as an issue. 

  var allImages = document.querySelectorAll("img");

  allImages.forEach(function(img){

    if (!img.hasAttribute("alt") || img.getAttribute("alt").trim() === "") {

      img.setAttribute("alt", "");
    }

  });

  // Fix: While we are here, we may as well remove the alt attribute mistakenly placed on the source element of the image module. 

  var allSource = document.querySelectorAll("source");

  allSource.forEach(function(source){

    source.removeAttribute("alt");
 
  });

}

// Accessibility Patch: Applied Filter

function fixAppliedFilter() {

  var appliedFilters = document.querySelectorAll(".search-results-options");

  appliedFilters.forEach(function(filter){

    var appliedFiltersList = filter.querySelectorAll("ul[aria-labelledby]");
    var btnSearchFilter = filter.querySelectorAll(".filter-button:not([disabled])");

    // Fix: Remove aria-labeledby from UL

    appliedFiltersList.forEach(function(list){

      list.removeAttribute("aria-labelledby");

    });

    // Fix: Remove aria-hidden and aria-expanded and add aria-describedby and role to parent element (.search-results-option)
    // TODO: Regarding aria-labelledby on section wrapper, see if contextual support good enough here. 
    // If it is, then we may be able to remove aria-labelledby and region role entirely. See https://www.w3.org/WAI/WCAG22/Techniques/html/H81
  
    filter.removeAttribute("aria-hidden");
    filter.removeAttribute("aria-expanded");
    filter.setAttribute("aria-labelledby", "applied-filters-label");
    filter.setAttribute("role", "group");

    // Fix: Add aria-label to each button that is not disabled. Include a better label that helps identify the functionality of the button.
    // BUG: When disabled filter button is present and last filter button is pressed, focus is placed on checkbox in filter section.

    btnSearchFilter.forEach(function(btn){
  
      btn.setAttribute("aria-label", labelRemoveFilter + " (" + btn.textContent + ")");

    });

  });

}

// Accessibility Patch: Cookie Management

function fixCookieManagement() {

  // Fix: Remove aria-describedby="cookieDescriptionIdAttr" from each input element.
  // Note: To fix, have product add @ to input aria-describedby. See https://app.screencast.com/9dzk8ozq864Pc (Thanks Paul!)

  var cookieDescriptionIdAttr = document.querySelectorAll("input[aria-describedby='cookieDescriptionIdAttr']");

  cookieDescriptionIdAttr.forEach(function(input) {

    input.removeAttribute("aria-describedby"); 

  });

}

// Accessibility: Patch: CMS Content

function fixCMS() {

  // Our CMS often strips needed HTML attributes, so this fix seeks to circumvent some of those removals. 

  // Fix: Lang Attributes are required to address shift in langauge in screen readers/AT. 

  const attributeLang = document.querySelectorAll("[data-cms-mb-lang]");

  attributeLang.forEach((el) => {

    el.setAttribute("lang", el.getAttribute("data-cms-mb-lang"));

  });

  // Fix: Tables created in CMS require scope attributes, which is beign stripped out by WYSIWYG editor. 

  const attributeScope = document.querySelectorAll("[data-cms-mb-scope]");

  attributeScope.forEach((el) => {

    el.setAttribute("scope", el.getAttribute("data-cms-mb-scope"));

  });

  // Fix: Add accessibly hidden new window text to CMS created links that open in new windows. Delivery will need to add new window icons for now. 

  const attributeTarget = document.querySelectorAll("a[target='_blank'][data-custom-category='UserCreatedContent']");

  attributeTarget.forEach((el) => {

    const span = document.createElement("span");
    span.classList.add("magicbullet-visually-hidden");
    span.textContent = " (" + labelNewWindow + ")";
    el.append(span);

  });

}

// Accessibility Patch: Data Form

function fixDataForm() {

  var dataForms = document.querySelectorAll(".data-form");

  dataForms.forEach(function(form){

    var instructionTexts = form.querySelectorAll(".instruction-text");
    var requiredLabels = form.querySelectorAll(".form-field.required label");
    var ariaRequired = form.querySelectorAll(".form-field.required input:not([type='checkbox']), .form-field.required select, .form-field.required textarea");
    var requiredXHTML = form.querySelectorAll("*[required='required']");
    var validationMsg = form.querySelectorAll(".field-validation-valid");
    var requiredFields = form.querySelectorAll(".form-field.required input, .form-field.required select");
    var dataFormElement = form.querySelectorAll("input, select");
    var autoCompleteFirstName = form.querySelectorAll("input[name='FirstName']");
    var autoCompleteLastName = form.querySelectorAll("input[name='LastName']");
    var autoCompleteAddress = form.querySelectorAll("input[name='StreetAddress']");
    var autoCompleteCity = form.querySelectorAll("input[name='City']");
    var autoCompleteEmailAddress = form.querySelectorAll("input[name='EmailAddress']");
    var autoCompletePhoneNumber = form.querySelectorAll("input[name='MobilePhone']");
    var autoCompleteZipCode = form.querySelectorAll("input[name='PostalCode']");
    var autoCompleteStateProvince = form.querySelectorAll("select[name='Custom-92392']"); // Targeing custom filed; need better way to handle this, probably product side fix eventually.
    var addJobAlertButtons = form.querySelectorAll(".keyword-add");
    var keywordSelected = form.querySelectorAll(".keyword-selected");
    var fileUploadButtons = form.querySelectorAll(".form-field input[name='Resume']");
    var captchaBadge = form.querySelector(".grecaptcha-badge");
    var signUpButtons = form.querySelectorAll("button[type='submit']");
    var formMessage = form.querySelector(".form-field.form-message b");
    var formMessageButton = form.querySelector(".form-field.form-message a");
    var keyWordCategory = form.querySelector(".keyword-category");

    // Clean-up: Remove empty instruction-text spans as they can sometimes cause undesired spacing issues.

    instructionTexts.forEach(function(element) {

      if (element.textContent.trim() === "") {
    
        element.parentNode.removeChild(element);
    
      }
    
    });
    
    // Clean-up: Remove aria-required from various elements. This attribute is sometimes flagged in automated testing and just the boolean required attribute is now recommended.
    
    ariaRequired.forEach(function(element) {
    
      element.removeAttribute("aria-required");
    
    });
    
    // Clean-up: required="required" is XHTML serialization and may throw a11y validation issues if not set to blank or true.
    
    requiredXHTML.forEach(function(element) {
    
      element.setAttribute("required", "");
    
    });

    // Fix: Remove the CSS asterisk (see init.scss) because it reads out to assistive tech (AT) when added via content property and include a span with aria-hidden on it so that it is not picked up by AT. New asterisk applied via CSS.

    var iconClassName = "ico-required-indicator";
    var iconClass = "." + iconClassName;

    requiredLabels.forEach(function(label) {

      // Fix: Remove textNodes that are inline astericks, instead of using a pseudo-element. 

      var labelTextNode = label.childNodes[0]; 

      if (labelTextNode && labelTextNode.nodeType === Node.TEXT_NODE) {

        // var cleanedText = labelTextNode.nodeValue.trim().replace("*", ""); // Removing trim as it it causing some layout issues. labels with link in them, like privacy policy, were being trimmed, cauing pacing issues.

        var cleanedText = labelTextNode.nodeValue.replace("*", "");

        // Update the text node without the asterisk

        labelTextNode.nodeValue = cleanedText;

      }

      var span = document.createElement("span");
      span.classList.add(iconClassName);
      span.setAttribute("aria-hidden", "true");
      span.textContent = "*";

      // See if icon we wish to append already exists.

      var getRequiredIcon = label.querySelector(iconClass);

      if(getRequiredIcon === null) {

        // HACK: This check ensures that the asterisk is only added to labels that are actually require it. 
        // Currently, the required class is applied to the fieldset itself, causing an asterisk to be added 
        // to all labels within the keyword area section, even when the associated field is not required.

        if (label.nextElementSibling?.matches("[class*='required']") || label.previousElementSibling?.matches("[class*='required']") || label.parentElement?.matches("[class*='required']")) {

          label.appendChild(span);

        }

      }

    });

    // Fix: Remove "role" from field-validation-error (it's not needed).

    validationMsg.forEach(function(element) {

      element.removeAttribute ("role");

    });

    // Fix: All required fields should include aria-invalid="false" on page load.

    requiredFields.forEach(function(input) {

      if (input.classList.contains("input-validation-error")) {

        input.setAttribute("aria-invalid", "true");

      } else {

        input.setAttribute("aria-invalid", "false");

      }

    });

    // Fix: The input-validation-error class is removed when leaving a field, but aria-invalid remains set to true, so we address this by listening for blur event.

    // Add blur event listener to each element

    dataFormElement.forEach(function(element) {

      element.addEventListener("blur", function() {

          if (element.classList.contains("input-validation-error")) {

            element.setAttribute("aria-invalid", "true");

          } else {

            element.setAttribute("aria-invalid", "false");

          }

      });

    });

    // Fix: It is customary to include more useful autocomplete attributes so that certain fields will show the contextual menu for easier input. Adding autocompletes for First Name, Family Name, and Email. For email we are also changing the type from "text" to "email"

    // First Name

    autoCompleteFirstName.forEach(function(input) {

      input.setAttribute("autocomplete", "given-name");

    });

    // Last Name

    autoCompleteLastName.forEach(function(input) {

      input.setAttribute("autocomplete", "family-name");

    });

    // Email Address

    autoCompleteEmailAddress.forEach(function(input) {
      
      input.setAttribute("type", "email");
      input.setAttribute("autocomplete", "email");
      
    });

    // Phone Number

    autoCompletePhoneNumber.forEach(function(input) {
      
      input.setAttribute("autocomplete", "tel");
      
    });

    // Zip Code

    autoCompleteZipCode.forEach(function(input) {
      
        input.setAttribute("autocomplete", "postal-code");
          
    });

    // State/Province

    autoCompleteStateProvince.forEach(function(input) {
      
      input.setAttribute("autocomplete", "address-level1");
            
    });

    // Street Address

    autoCompleteAddress.forEach(function(input) {
      
      input.setAttribute("autocomplete", "street-address");
                
    });

    // City

    autoCompleteCity.forEach(function(input) {
      
      input.setAttribute("autocomplete", "address-level2");
            
    });

    // Fix: The "Add" button needs to be more explicit to AT so include "Add Job Alert"

    addJobAlertButtons.forEach(function(button) {

      button.setAttribute("aria-label", labelAddJobAlert);

    });

    // Fix: The keyword list requires a more accessible grouping to better identify it; adding proper role and accName, etc.

    var keywordSelectedRegionClassName = "keyword-region";
    var keywordSelectedRegionClass = "." + keywordSelectedRegionClassName;
    var keywordSelectedRegionLabel = labelSelectedJobAlerts;

    keywordSelected.forEach(function(region) {

      var keywordSelectedRegion = document.createElement("div");
      keywordSelectedRegion.classList.add(keywordSelectedRegionClassName);
      keywordSelectedRegion.setAttribute("role", "region");
      keywordSelectedRegion.setAttribute("aria-label", keywordSelectedRegionLabel);

      // See if region we wish to append already exists.

      var getKeywordRegion = form.querySelector(keywordSelectedRegionClass);

      if(getKeywordRegion === null) {

        region.parentNode.insertBefore(keywordSelectedRegion, region);
        keywordSelectedRegion.appendChild(region);

      }

    });

    // Fix: The keyword add section requires better focus management.

    // Issue: When adding new keywords, focus is lost after adding, so we will move focus to first keyword select.

    const keywordFieldsetSelector = "fieldset.form-field";

    const keywordAdd = form.querySelectorAll(`${keywordFieldsetSelector} .keyword-add`);

    keywordAdd.forEach((button) => {

      button.addEventListener("click", () => {

        const fieldset = button.closest(keywordFieldsetSelector);

        if (!fieldset) return;

        const firstSelect = fieldset.querySelector(".keyword-category");

        if (!firstSelect) return;

        firstSelect.focus();

      });

      const keywordRegion = button.closest(keywordFieldsetSelector)?.querySelector(".keyword-region");

      if (keywordRegion) {

        // We need a mutation observer here to see when link is being added, so we can add a role to it. 

        const keywordRemoveObserver = new MutationObserver(() => {

          keywordRegion.querySelectorAll(".keyword-remove").forEach((link) => {

            link.setAttribute("role", "button");

          });

        });

        keywordRemoveObserver.observe(keywordRegion, { childList: true, subtree: true });

      }

    });

    // Issue: When a keyword is removed, focus is lost, so we will now manage it by placing it on next available element.
    // When all keywords are removed, focus is then placed on the first keyword select element. 
    // Time will tell if this is the best place for it or if we should place it elsewhere, but it should be easy to adjust regardless.  

    const keywordFieldsets = form.querySelectorAll(keywordFieldsetSelector);

    keywordFieldsets.forEach((fieldset) => {

      if (fieldset.dataset.keywordRemoveInitialized) return;

      fieldset.dataset.keywordRemoveInitialized = "true";

      const handleKeywordRemove = (event) => {

        if (event.type === "keydown" && event.key !== "Enter" && event.code !== "Space") return;

        const link = event.target.closest(".keyword-remove");

        if (!link) return;

        event.preventDefault();

        const listItem = link.closest("li");

        if (!listItem) return;

        const allLinks = [...fieldset.querySelectorAll(".keyword-remove")];
        const currentIndex = allLinks.indexOf(link);
        const nextLink = allLinks[currentIndex + 1] || allLinks[currentIndex - 1];

        listItem.remove();

        if (nextLink) {

          nextLink.focus();

        } else {

          const firstSelect = fieldset.querySelector(".keyword-category");

          if (firstSelect) firstSelect.focus();

        }

      };

      fieldset.addEventListener("click", handleKeywordRemove);
      fieldset.addEventListener("keydown", handleKeywordRemove);

    });

    // Fix: The file upload remove button is a link with an href hash. This is awful, so let's remedy it by replacing it with a button.

    fileUploadButtons.forEach(function(input) {

      var fileUploadLink = input.nextElementSibling;
      var resumeRemoveLabel = fileUploadLink.textContent.trim();

      var fileUploadButton = document.createElement("button");
      fileUploadButton.setAttribute("aria-describedby", input.getAttribute("id"));
      fileUploadButton.classList.add("file-remove");
      fileUploadButton.style.display = "none";
      fileUploadButton.textContent = resumeRemoveLabel;

      fileUploadLink.parentNode.replaceChild(fileUploadButton, fileUploadLink);

    });

    // Fix: Adding an accName to textarea, removing iframe garbage, and moving captcha to end of form to address tab order. It should not exist before submit button.

    if(captchaBadge) {

      var captchaResponse = captchaBadge.querySelector(".g-recaptcha-response");
      var captchaResponseLabel = labelCaptcha;
      var captchaIframe = captchaBadge.querySelectorAll("iframe");

      // Add accName to textarea

      captchaResponse.setAttribute("aria-label", captchaResponseLabel);

      // Remove iframe junk (border: 0 now set in init.scss)

      captchaIframe.forEach(function(iframe){

        iframe.removeAttribute("frameborder");

      });

    }

    // Fix: The "Sign Up" button needs to be more explicit to AT.
    // TODO: Add language support.

    var signUpButtonLabel = labelSubmitJobAlerts;

    signUpButtons.forEach(function(button) {

      button.setAttribute("aria-label", signUpButtonLabel);

    });

    // Fix: The form message has an inline tabindex="0" on it. This is not ideal as messages that receive focus should only do so temporarily and not when user tabs back to it.

    if(formMessage) {

      formMessage.setAttribute("tabindex", "-1");

    }

    // Fix: The form message close link should really be a button, but for now we'll simply add a role.
    // TODO: Unbind or override current close event and add support for closing when Enter AND Spacebar is pressed.

    if(formMessageButton) {

      formMessageButton.classList.add("data-form__close");
      formMessageButton.setAttribute("role", "button");

      var formMessageButtonIcon = document.createElement("span");
      formMessageButtonIcon.setAttribute("aria-hidden", "true");
      formMessageButtonIcon.classList.add("data-form__closeicon");

      formMessageButton.appendChild(formMessageButtonIcon);

    }

    // Form submission events

    form.addEventListener("submit", function(event) {

      event.preventDefault();

      // Fix: The Keyword Location field does not appear to have an aria-describedby on it when an error is returned, so we need to grab it from Keyword Category and dupe it here.

      if(keyWordCategory) {

        var keyWordLocationDesc = keyWordCategory.getAttribute("aria-describedby");

        var keywordLocation = form.querySelector(".keyword-location");

        if(keywordLocation) {

          keywordLocation.setAttribute("aria-describedby", keyWordLocationDesc);

        }

      }

    });

  });

}

// Accessibility Patch: Global Disclosure

function fixGlobalDisclosure() {

  var expandableParentBtn = document.querySelectorAll(".expandable-parent");

  expandableParentBtn.forEach(function(button) {

    // Fix: See if element is already open, set aria-expanded state to true if it is.

    if(button.classList.contains("expandable-child-open")) {

      button.setAttribute("aria-expanded", "true");
  
    } else {

      button.setAttribute("aria-expanded", "false");

    }

    // Fix: Remove aria-expanded from adjacent, non-interactive element.
  
    if (button.nextElementSibling) {
  
      button.nextElementSibling.removeAttribute("aria-expanded");
      button.nextElementSibling.removeAttribute("aria-hidden");

      // TODO: Instead of targeting nextElementSibling, go to parent element and querySelector(".search-filter-list"). 
      // Then we can move the aria-labelledby and role on the section element down to it's own div below the button. 
      // Great Clips brought this up recently and it is good point. See https://hub.accessible360.com/issues/454261/edit#comment-801093 
      // See note.txt
  
    }

    // Fix: New toggle functionality for newly added aria-expanded attribute.

    button.addEventListener("click", function() {

      if(this.classList.contains("expandable-child-open")) {

        this.setAttribute("aria-expanded", "true");

      } else { 

        this.setAttribute("aria-expanded", "false");

      }

      // Fix: Remove aria-expanded, aria-hidden being added to adjacent, non-interactive element, by CS Core.

      if (this.nextElementSibling) {

        this.nextElementSibling.removeAttribute("aria-expanded");
        this.nextElementSibling.removeAttribute("aria-hidden");

      }

    });

  });

}

// Accessibility Patch: Iframe Elements

function fixIframeElement() {

  // Fix: Find YouTube embeds that may be missing the title attribute and add a generic one.

  var missingTitleYoutube = document.querySelectorAll("iframe[src*='https://www.youtube.com/']:not([title])");

  missingTitleYoutube.forEach(function(title){

    title.setAttribute("title", "Youtube video player");

  });

  var missingTitleVimeo = document.querySelectorAll("iframe[src*='https://player.vimeo.com/']:not([title])");

  missingTitleVimeo.forEach(function(title){

    title.setAttribute("title", "Vimeo video player");

  });

}

// Accessibility Patch: Input Elements (Utility)

function fixInputElements() {

  // Fix: Input elements with type of "checkbox" should not contain autocomplete as it is not a text or select field, so we need to remove.

  var inputCheckBox = document.querySelectorAll("input[type='checkbox']");

  inputCheckBox.forEach(function(input) {

    input.removeAttribute("autocomplete");

  });

}

// Accessibility Patch: Mindreader Combobox

function fixMindReaderInput() {

  var comboBoxInput = document.querySelectorAll(".search-location, .keyword-location");

  comboBoxInput.forEach(function(input) {

    // Precompute IDs and frequently used elements
    
    var inputId = input.getAttribute("id");
    var label = document.querySelector("label[for=" + inputId + "]");
    var mindReaderID = inputId + "-mindreader";
    var mindReader = document.getElementById(mindReaderID);
    var mindReaderStatus = document.getElementById(inputId + "-mindreader-status");

    // Set Autocomplete Description 

    var autoCompleteID = document.getElementById("autocomplete-message-" + inputId);

    if (!autoCompleteID) {

      var autoCompleteMessage = document.createElement("span");
      autoCompleteMessage.setAttribute("id", "autocomplete-message-" + inputId);
      autoCompleteMessage.setAttribute("hidden", "");
      autoCompleteMessage.textContent = "When autocomplete results are available use up and down arrows to review and enter to select. Touch device users, explore by touch or with swipe gestures.";
      input.parentNode.appendChild(autoCompleteMessage);

    }

    // Fix: Set unique ID on label

    if(label) {

      label.setAttribute("id", inputId + "-label");

    }

    // Fix: Prep Comboboxes with proper ARIA
    
    input.setAttribute("aria-autocomplete", "list");
    input.setAttribute("aria-haspopup", "listbox");
    input.setAttribute("aria-expanded", "false");
    input.setAttribute("autocomplete", "off");
    input.setAttribute("role", "combobox");
    input.setAttribute("aria-controls", mindReaderID);

    var currentDescribedBy = input.getAttribute("aria-describedby");

    var newDescriptionId = "autocomplete-message-" + inputId;

    if (currentDescribedBy) {

      if (!currentDescribedBy.includes(newDescriptionId)) {

        input.setAttribute("aria-describedby", currentDescribedBy + " " + newDescriptionId);

      }
    
    } else {
      
      input.setAttribute("aria-describedby", newDescriptionId);
    
    }

    // Fix: Update input as needed based on class presence
    
    function checkInput() {
    
      if(input.classList.contains("mindreader-results-open")) {

        input.setAttribute("aria-expanded", "true");
      
      } else {
    
        input.setAttribute("aria-expanded", "false");
        input.removeAttribute("aria-activedescendant");
    
      }
    
    }

    checkInput();

    // Function to check the active class in combobox list
    
    function checkActiveClass() {

      if(mindReader) {
    
        var activeItem = mindReader.querySelector("a.active"); 

        // BUG: See https://kpmg.runmytests.eu, Dropdown works initially, but on second try, does not apper to work correctly. Throwing folling, can't find mindReader ID. 
        // Uncaught TypeError: Cannot read properties of null (reading 'querySelector') at HTMLDocument.checkActiveClass (init.js:720:35)

        if (activeItem) {

          // Get li ID

          var listID = activeItem.parentElement.getAttribute("id");
  
          // Remove redundant screen reader announcements

          mindReaderStatus.textContent = "";

          // Pass selected ID to aria-activedescendant

          input.setAttribute("aria-activedescendant", listID);

        }

      }
    
    }

    // Add event listener for when focus leaves the input field (focusout)
    
    input.addEventListener("focusout", function() {
    
      checkInput();  // Update aria-expanded to false
    
    });

    // Add event listener for when focus leaves the input field (focusout)
    
    input.addEventListener("focusout", function() {
    
      checkInput();  // Update aria-expanded to false
    
    });

    // Add event listener for when Escape key is pressed

    input.addEventListener("keydown", function(event) {
  
      if (event.key === "Escape") {
    
        checkInput();  // Update aria-expanded to false

      }

    });

    // Listen for both keydown and keyup events

    document.addEventListener("keydown", checkActiveClass); 
    document.addEventListener("keyup", checkActiveClass);

  });

}

// Accessibility Patch: Mindreader List

function fixMindReaderList() {

  // Fix: Add role of listbox to each UL

  var mindReaderLists = document.querySelectorAll(".mindreader-results, .typeahead");

  mindReaderLists.forEach(function(list, index) {

    // Precompute list attributes

    var listID = list.getAttribute("id");
    var ariaLabelledBy = listID.replace("-mindreader", "-label");

    // Add ARIA attributes to each list

    list.setAttribute("role", "listbox");
    list.setAttribute("aria-labelledby", ariaLabelledBy);

    // Process each list item

    var items = list.querySelectorAll("li");

    items.forEach(function(item, itemIndex) {

      // Generate unique IDs for options

      var optionID = "combobox-item-" + (index + 1) + "-" + (itemIndex + 1);

      // Add role and ID to each item

      item.setAttribute("role", "option");
      item.setAttribute("id", optionID);

    });

  });

}

// Accessibility Patch: Header (Utility) - Fixes common issues in header. 

function fixHeader() {

  // Fix: See if nav in header is missing an accName. Note: Might be issue for a header with two navs. 

  var headerNavigation = document.querySelector("header nav");

  if(headerNavigation) {

    if (!headerNavigation.hasAttribute("aria-label")) {

      headerNavigation.setAttribute("aria-label", labelPrimaryNavigation);

    }

  }

}

// Accessibility Patch: Job Description (Utility)

function fixJobDescription() {

  var atsDescription = document.querySelectorAll(".ats-description");

  atsDescription.forEach(function(desc) {

    // Fix: Remove tabindex attribute from elements within .ats-description that have tabindex attribute not equal to "0" or starting with "-""

    desc.querySelectorAll("[tabindex]:not([tabindex='0']):not([tabindex^='-'])").forEach(function(tabindex) {
  
      tabindex.removeAttribute("tabindex");

    });

    // Fix: Add role="presentation" to tables within .ats-description

    desc.querySelectorAll("table").forEach(function(table) {
  
      table.setAttribute("role", "presentation");

    });

    // Fix: Remove useless attributes from all elements within .ats-description

    desc.querySelectorAll("*").forEach(function(element) {
  
      element.removeAttribute("face");
      element.removeAttribute("size");
      element.removeAttribute("title");
      element.removeAttribute("id");

    });

    // Fix: Remove <font> element and unwrap its contents within .ats-description

    desc.querySelectorAll("font").forEach(function(font) {
  
      var parent = font.parentNode;
  
      while (font.firstChild) {
  
        parent.insertBefore(font.firstChild, font);
  
      }
  
      parent.removeChild(font);

    });

    // Fix: Add hidden new window text to links that open in new windows. Delivery will need to add new window icons for now.  

    const attributeTarget = desc.querySelectorAll("a[target='_blank']");

    attributeTarget.forEach((el) => {

      const span = document.createElement("span");
      span.classList.add("magicbullet-visually-hidden");
      span.textContent = " (" + labelNewWindow + ")";
      el.append(span);

    });

  });

}

// Accessibility Patch: Job List

 // Fix: Move location in into Job List link.

/* function fixJobList() {

  Removing for now. Paul brought interesting issue to my attention where job lists have multiple labels, in addition to location, so placing
  an element in previous element is causing issues. Revisit the markup of this component with the Base GST amd Site Admin team. 

  var jobListElements = document.querySelectorAll(".job-list .location, .job-list .date");

  jobListElements.forEach(function(element) {

    var previousElement = element.previousElementSibling;
  
    if (previousElement) {
  
      previousElement.appendChild(element);
  
    }

  });

} */

// Accessibility Patch: Job Location

function fixJobLocation() {

  // Fix: Job map links should really be buttons, not links. Including role="button" for now, but we need to add spacebar key support eventually.
  // BaseGST: Unfortunately, the team cannot change these to buttons in GST, so this will need to be a product request.

  var mapButton = document.querySelectorAll(".job-map-nearby a");

  mapButton.forEach(function(btn){

    btn.setAttribute("role", "button");

  });

  // Fix: Google Form Related Fixes on Job Location Pages

  var mapFormInputs = document.querySelectorAll(".job-map-directions input");

  // Clean-up: Remove aria-required from various elements. This attribute is sometimes flagged in automated testing and just the boolean required attribute is now recommended.
  // Clean-up: required="required" is XHTML serialization and may throw a11y validation issues if not set to blank or true.
  // Base GST: This issue has been addressed and can eventually be removed.
 
  mapFormInputs.forEach(function(element) {
    
    element.removeAttribute("aria-required");
    element.setAttribute("required", "");
  
  }); 

  // TODO: The "Search Nearby" and "Get Directions" sections should be regions with accNames.
  // TODO: Include Wegmans functionality to skip over Google Map.

}

// Accessibility Patch: Job Map
// Wxample: https://greatclips-refresh-1.runmytests.com/jobs-map 

function fixJobMap() {

  var jobMapInputs = document.querySelector("#sel-state, #tbx-zip")
  var jobMapCityWrapper = document.querySelector("#wrapper-sel-city");

  if (jobMapInputs && jobMapCityWrapper) {

    // Since the select is blank on page load, it should not be shown until action taken in either the state or zip dropdowns. 

    jobMapCityWrapper.setAttribute("hidden", "");

    jobMapInputs.addEventListener("change", function() {

      jobMapCityWrapper.removeAttribute("hidden");

    });

  }

}

// Accessibility Patch: Save Job Button 

function fixSaveJobButton() {

  const btnSaveJobs = document.querySelectorAll(".js-save-job-btn");

  btnSaveJobs.forEach((btn) => {

    // Fix: Custom label needed to override the text toggle that delivery often adds (or removes). Text changes should never be used to convey state.
    // TODO: Add language support.

    btn.setAttribute("aria-label", labelSaveJob);

    // Fix: iOS, NVDA bug, state not reading back so need to implicitly add a role of "button" and remove type attribute. Do not remove until support better.

    btn.removeAttribute("type");
    btn.setAttribute("role", "button");

    // Fix: aria-pressed required, which will properly convey state of the button. Load correct state based on data-job-saved attr.

    if(btn.dataset.jobSaved === "true") {

      btn.setAttribute("aria-pressed", "true");

    } else {

      btn.setAttribute("aria-pressed", "false");

    }

    btn.addEventListener("click", function() {

      const pressedState = this.dataset.jobSaved === "true" ? "false" : "true";
      const jobId = this.dataset.jobId;
      const relatedSaveBtns = document.querySelectorAll(`.js-save-job-btn[data-job-id="${jobId}"]`);

      // The reason we target data-job-id is in event two or more buttons exist on the page for the same job. Job detail pages often have more than one. 

      relatedSaveBtns.forEach((saveBtn) => {
    
        saveBtn.setAttribute("aria-pressed", pressedState);

      });

    });

  });

  // Fix: Invoke a MutationObserver to watch the Saves Jobs link for changes. It add additional information to link based on whether any jobs have been saved. 
  // This additional text will better serve AT users. 

  // Note: Mutation Observers work better when you do not add them to click events. This is what I get for listening to AI.

  const recentlyViewedJobListTarget = "[data-selector-name=recentlyviewedjoblist]";
  const savedJobsLink = "a";

  const updateSavedJobsLink = () => {

    const recentlyViewedJobList = document.querySelector(recentlyViewedJobListTarget);
  
    if (!recentlyViewedJobList) return;

    const isActive = recentlyViewedJobList.getAttribute("data-recently-viewed-jobs") === "true";

    let link = recentlyViewedJobList.closest(savedJobsLink) || recentlyViewedJobList.querySelector(savedJobsLink);

    if (!link) return;

    let labelText = link.textContent.trim();

    const numberMatch = labelText.match(/\p{Nd}+/u);

    let number = null;

    if (numberMatch) {
  
      number = numberMatch[0];

      // Use a regex to remove the first occurrence of that number safely
  
      labelText = labelText.replace(new RegExp(number), "").trim();

    }

    const savedJobsLabel = number ? `${number} ${labelText} (${labelViewSavedJobs})` : `${labelText} (View all)`;

    if (isActive) {
    
      link.setAttribute("aria-label", savedJobsLabel);
  
    } else {
  
      link.removeAttribute("aria-label");
  
    }

  };

  const recentlyViewedParent = document.querySelector(recentlyViewedJobListTarget)?.parentNode; 

  if (recentlyViewedParent) {

    const observer = new MutationObserver(updateSavedJobsLink);

    observer.observe(recentlyViewedParent, {
  
      subtree: true,
      childList: true,
      attributes: true,
      attributeFilter: ["data-recently-viewed-jobs"]

    });

  }

  // Note: It is probably not a good idea to fire off this function after the mutation observer has run, but doing so for sake of speed to finish for Citi. Will revisit. 
  // Considering merging this into the click event for save job button. 

  updateSavedJobsLink();

}

// Accessibility Patch: Search Form

function fixSearchForm() {

  var searchForm = document.querySelectorAll(".search-form");

  searchForm.forEach(function(form, i){

    var formID = (i + 1);
    var searchFormLegend = form.querySelector(".job-search-legend");
    var searchFormFields = form.querySelector(".search-form-fields, .search-form__fields");
    var searchFormLocationInput = form.querySelector(".search-location");
    var searchFormLocationPin = form.querySelector(".location-pin");
    var searchFormLocationError = form.querySelector(".search-location-error, .search-form__location-error");
    var searchFormSubmit = form.querySelector("button, .search-form__search-btn");

    // Shared Function(s)

    function accessibleValidation() {

      setTimeout(function() {

        searchFormLocationError.removeAttribute("tabindex");

        var ariaHiddenHook = searchFormLocationError.getAttribute("aria-hidden");

          if(ariaHiddenHook === "false") {

            searchFormLocationInput.setAttribute("aria-invalid", "true");
            searchFormLocationInput.focus();

          } else { 

            searchFormLocationInput.setAttribute("aria-invalid", "false");

          }

      }, 100);

    }

    // Fix: Our primary search form should include a distinct role so that it can aid in helping assistive technology users navigate the page, so adding role="search"

    form.setAttribute("role", "search");

    if(searchFormFields) {

      // Fix: Our primary search form should provide a better group description for the fields at hand. This can be achieved by adding a div with a class of "job-search-legend" (a heading should not be used) and the following script will take care of adding any needed associations by either adding aria-describedby or aria-label, with appropriate IDs.
    
      searchFormFields.setAttribute("role", "group");

      // Fix: Remove aria-expanded from .search-form-fields. 

      searchFormFields.removeAttribute("aria-expanded");

    }

    if(searchFormLegend) {

      // Think of this as a fieldset and legend element.

      searchFormFields.setAttribute("aria-labelledby", "job-search-legend-" + formID);
      searchFormLegend.setAttribute("id", "job-search-legend-" + formID);

    } else {

      // If the class (.job-search-legend) is not present then simply add a label of "Search Jobs" to the div wrapping the form elements (.search-form-fields)

      if(searchFormFields) {
      
        searchFormFields.setAttribute("aria-label", labelSearchJobs);

      }

    }

    // Fix: The way in which the validation is currently handled requires improvement to aid those using assistive technology and keyboard. Here we need to include aria-invalid and aria-describedby to the locations field and apply custom validation as well, so that keyboard users do not need to tab backwards to return to the locations field, which is initiating the error.

    if(searchFormLocationInput) {

      // Add aria-describedby and aria-invalid to the locations field. 

      searchFormLocationInput.setAttribute("aria-describedby", "search-error-" + formID);
      searchFormLocationInput.setAttribute("aria-invalid", "false");

      // Validate the locations field when change is made to it.

      searchFormLocationInput.addEventListener("change", function() {

        accessibleValidation();
  
      });

    }

    // Fix: Apply a unique ID to the error message. This is what locations field will read when focus is brought back to it, thanks to aria-describedby.

    if(searchFormLocationError) {

      searchFormLocationError.setAttribute("id", "search-error-" + formID);

    }

    // Fix: When search button is pressed, fire off custom validation. 

    searchFormSubmit.addEventListener("click", function() {

      accessibleValidation();

    });

    // Fix: Remove aria-hidden from the location pin. Often this is visually displayed, but aria-hidden is still present.
    // We need to check the visability of the elmn before removing aria-hidden so it does not render on screen if featrue not activated in admin. 

    if(searchFormLocationPin && window.getComputedStyle(searchFormLocationPin).display !== "none") {

      searchFormLocationPin.removeAttribute("aria-hidden");

    }

  });

}

// Accessibility Patch: Search Filters

function fixSearchFilters() {

  const searchFilters = document.querySelector("#search-filters");

  if (searchFilters) {

    // Fix: Add role and accName to Search Filter section. 
    // TODO: Add langauge support.
    
    searchFilters.setAttribute("role", "region");
    searchFilters.setAttribute("aria-label", "Search Filter");

    // TODO: It would be great if we could use aria-labelledby instead of aria-label as accName for region so that we would not requre custom translation, but heading has no distinct class to leverage. Look into adding unique ID to h2 instead maybe?

    // Fix: Each section element (`.expandable`) should contain a role of "group" as well as an accName. We will need to add an ID to each button for aria-labelledby.

    const sectionElement = searchFilters.querySelectorAll(".expandable");

    sectionElement.forEach((section) => {

      const sectionButtonID = section.querySelector(".expandable-parent").getAttribute("id");

      section.setAttribute("role", "group");
      section.setAttribute("aria-labelledby", sectionButtonID);

    });

    // Refined Search

    const refinedSearch = searchFilters.querySelector("#refined-search");

    if (refinedSearch) {

      const keyWordInput = refinedSearch.querySelector("#keyword-tag");
      const keywordError = refinedSearch.querySelector(".keyword-tag-error");

      // Fix: Add unique ID to error message and reference ID on input. When focus placed here, message will be read to AT. 

      keywordError.setAttribute("id", "keyword-error-msg");
      keyWordInput.setAttribute("aria-describedby", "keyword-error-msg");

      // Fix: Always place focus on input when error is visible

      const keyWordVisibility = keywordError.getAttribute("aria-hidden");

      if (keyWordVisibility === "false") {

        keyWordInput.focus();

      } 

      // Fix: Remove junk from error message

      keywordError.removeAttribute("aria-expanded");
      keywordError.removeAttribute("style");

    }

  }

}

// Accessibility Patch: Search Results

function fixSearchResults() {

  // Fix: Remove skip links from search results page. These are no longer needed. 

  var searchResultsSkip = document.querySelectorAll(".access-link[href='#search-results-list'], .access-link[href='#search-filters']");

  searchResultsSkip.forEach(function(link){

    link.remove();

  });

  // Fix: Let user know how many results are now present on page, after an update: 
  // TODO: This appears to work well with filters, but not working with pagination. 

  const searchResultsContainer = document.querySelector("#search-results");
  const ariaMsg = document.querySelector("#magicbullet-message");

  // Note: Wondering if nesting an observer inside of a function that load based on parent observer is wise.
  // Fundamentally, this may dictate how observers work across patch. Fine for now, but need to revisit. 

  const searchResultsObserver = new MutationObserver(() => {

    // Recalculate count each time the DOM changes

    const searchResultTotal = searchResultsContainer.dataset.totalResults;
    const searchResultCount = searchResultsContainer.querySelectorAll("#search-results-list > ul > li").length;
    ariaMsg.textContent = searchResultCount + " of " + searchResultTotal + " results are now available.";

  });

  if (searchResultsContainer) {
  
    searchResultsObserver.observe(searchResultsContainer.parentNode, {childList: true, subtree: true});

  }

}

// Accessibility Patch: Search Results Pagination

function fixSearchPagination() {

  const searchResults = document.querySelector("#search-results");

  if (!searchResults) return;

  // There can be multiple pagination components on a page, tho it is common to only see one, typically at the bottom of all search results. 
  
  // Fix: Remove title attributes from all "pagination-view-more" buttons. These are often redundant and read back twice to assistive technology. I recall asking product team to remove. 
  
  const viewMoreBtns = searchResults.querySelectorAll(".pagination-view-more");

  viewMoreBtns.forEach((btn) => {

    btn.removeAttribute("title");

  });

  const searchResultsPagination = searchResults.querySelectorAll(".pagination");

  searchResultsPagination.forEach((pagination) => {

    // Fix: Pagination(s) in Search Results should must have an accName so that they can be distinct amoung other navigational landmarks.

    if (!pagination.hasAttribute("aria-label")) {

      pagination.setAttribute("aria-label", labelPagination);

      // TODO: Add language support for "Pagination"

    }

    // Fix: Search Results pagination disabled button can be tabbed to (this is bad). To address this (along with console error being produced when focus is 
    // placed on an element with aria-hidden), we simply remove the href. Also removing rel and text as well.

    const paginationBtnDisabled = pagination.querySelector(".disabled");

    if (paginationBtnDisabled) {

      paginationBtnDisabled.removeAttribute("href");
      paginationBtnDisabled.removeAttribute("rel");
      paginationBtnDisabled.setAttribute("aria-disabled", "true");

      // Note: Ideally, it would be best to remove this element entirely with CSS, but this can break layout, so removing href is next best solution. 

    }

    // Fix: Remove superflous content hidden with label. This lable appears to be present to provide instructions to AT users, which is not really needed. 

    const labelInstructions = pagination.querySelector(".pagination-current-label b");

    if (labelInstructions) {

      labelInstructions.remove();
    
    }

    // Fix: When Pagination buttons are pressed, send a loding message to global ARIA live. 

    const paginationEvents = () => {

      const ariaMsg = document.querySelector("#magicbullet-message");

      if (ariaMsg) {

        ariaMsg.textContent = "Loading New Results";

      }

      // Observe the PARENT because #search-results may be replaced

      const parent = searchResults.parentNode;

      const searchResultsNavigation = new MutationObserver(() => {

        const getFirstLink = document.querySelector("#search-results a");

        getFirstLink.focus();
        searchResultsNavigation.disconnect();

      });

      // Watch for ANY child changes under the parent. We need to do this because #search-results is replaced instead of it's contents being replaced. 

      searchResultsNavigation.observe(parent, { 

        childList: true, 
        subtree: true 

      });

    }

    const paginationBtns = pagination.querySelectorAll(".pagination-current, .pagination-page-jump, .prev, .next");

    paginationBtns.forEach((btn) => {

      btn.addEventListener("click", paginationEvents);

      // Input fields (".pagination-current") get Enter key support

      if (btn.matches(".pagination-current")) {
        
        btn.addEventListener("keydown", (event) => {
          
          if (event.key === "Enter") {
          
            paginationEvents();
          
          }
        
        });
      
      }

    });

  });

}

// Accessibility Patch: Sitemap

function fixSitemap() {

  // Fix: Remove `tabindex`, `aria-expanded`, and `.expandable-parent` from `.job-location h2` and `.job-category h2`.
  // TODO: Investigate possibly removing this. Tabindex may have been addressed by product. 

  var sitemapHeadings = document.querySelectorAll(".job-location h2, .job-category h2");

  sitemapHeadings.forEach(function(heading) {

    heading.removeAttribute("tabindex");
    heading.removeAttribute("aria-expanded");
    heading.classList.remove("expandable-parent");

  });

}

// Accessibility Patch: Social Share

function fixSocialShare() {

  // Fix: Append the following element and helper text, `<span class=".magicbullet-visually-hidden">(Opens in new tab or window)</span>`, to each `.social-share-items a` element.

  // TODO: Add language support.

  var socialShareLinks = document.querySelectorAll(".social-share__item a[target='_blank']");

  socialShareLinks.forEach(function(link) {

    var span = document.createElement("span");
    span.classList.add("magicbullet-visually-hidden");
    span.textContent = labelNewWindow;
    link.append(span);

    // NICE TO HAVE: We don't really need the rel attribute anymore. Ask prodcut to eventually remove it.

  });

}
/*!

  Radancy MagicBullet: Accessibility Patch

  Contributor(s):
  Michael "Spell" Spellacy

*/

/* TODO */

/* 

  Consider adding pseudo generated alt text to all links on site that open in new windows, may need to remove visually hidden JS inserted content 

  Example: content: "\f35d" / "(opens in new window)";

*/

/* Global Issues */

/* Visually Hidde Content */

.magicbullet-visually-hidden:not(:focus):not(:active) {
clip: rect(0 0 0 0); 
clip-path: inset(50%);
height: 1px;
overflow: hidden;
position: absolute;
white-space: nowrap; 
width: 1px;
}

/* Ensure that all placeholder text is contrast friendly. */

::-moz-placeholder {
color: curretColor;
opacity: 1;
}
    
::-webkit-input-placeholder {
color: currentColor;
}

/* Hide Empty Elements */

h1, h2, h3, h4, h5, h6, label, a {

    &:empty {
    display: none;
    }

}

/* Access Link - Ensure accesslink always has _highest_ index. */

.access-link {
z-index: 2147483647 !important;
}

/* Search Location Datalist */

.a11y-search-location {

    .mindreader-results, .mindreader-status, .display-typeahead {
    display: none !important;
    }

}

/* Mindreader */

.mindreader-results {
padding: 0;

  /* Fix: Always set each autocomplete link to block, so that we can... */

  a {
  display: block;
  padding: .5em 1em;

    /* ... highlight the background, which is not always done. */ 

    &.active {
    background-color: #eee;
    outline: 1px #000 solid;
    outline-offset: -4px;
    }

  }

}

.a11y-search-location { 

  .search-location {
  z-index: 0 !important;
  }

}

/*  Data Forms */

.data-form {

  .form-field {
    
    &.required {
        
      label {

        /* A11YFORM001 */

        &::after {
        display: none;
        }

        .ico-required-indicator {
        display: inline-block;
        margin-inline-start: 0.5em;
        }

      }

    }

  }

  .data-form__close {
    
    &::after {
    display: none !important;
    }

  }

  .data-form__closeicon {
  display: block;
  pointer-events: none;

    &::after {
    border-radius: 10px;
    color: inherit;
    content: "+";
    display: inline-block;
    font: normal calc(24em/16) / .65em Arial, sans-serif;
    margin: -2px 0 0 10px;
    padding: 4px;
    position: absolute;
    right: 4px;
    text-indent: 0;
    text-shadow: none;
    text-transform: lowercase;
    top: 0;
    transform: rotate(45deg);
    } 

  }

  /* legends do not require an asterick */

  legend {

    &::after {
    display: none;
    }

  }

}

/* removing frameborder */

.grecaptcha-badge {

  iframe {
  border: 0;
  }

}

/* Search Form: Basic */

.search-form {

  .search-location-error, .search-form__location-error {
  outline: 0;
  }

}

/* 

  Since we are removing aria-expanded from various, non-interactive elements, we need to make it is not impacting ajax functionalit that may look them and keep component open.
  Fortunately, we can use the open class to our advantage. 

*/

.search-filter-list, .search-filters__list {
    
  &.expandable-childlist-open {
  display: block;
  }

}

/* 

  On mobile, when the search filter is open, the entire page can be accidentialy refreshed when scrolling. This is aggrivating, causing user to have open the filers again. 
  Also adding default color to scrollbar as default often not discernable

*/

#search-filters {

  &.open {
  overscroll-behavior: none;
  }

}

/* The typeahead text sometimes interferes with the text being typed, making things illegible. Removing for now as I do not it is absolutely necessary to include. The dropdown should be sufficient. */

.display-typeahead {
display: none;
}

/* The comboxbox on the Job Map page is severaly broken for accessibility. An easier way to address this issue is to always have the <select> present. */

#wrapper-sel-city {
height: auto;

  &[hidden] {
  display: none !important;
  }

}

#sel-city, label[for="sel-city"] {
display: block !important;

  option:first-child {
  display: none;
  }

}

label[for="sel-cityundefined"], .combobox-container {
display: none !important;
}

.keyword-region { 
display: contents;
}

/*

AJD "Back to Navigation" Links
Issue: WAVE is reporting contrast issue due to opacity: 0 on the hyperlinks. To override, we set opacity to 1 or auto and hide the link using clip. Allso ensuring good contrast.

*/

.inPageNav__back-link, .back-to-ajd-nav__a {
background-color: #666;
clip: rect(0, 0, 0, 0);
color: #fff;
opacity: 1;

  &:focus {
  background-color: #666;
  clip: auto;
  }

}

/* Refine Keyword */

#refined-search {

  #add-keyword[disabled], #add-keyword[disabled]:hover, #add-keyword[disabled]:focus, #add-keyword[disabled]:focus-visible {
  background-color: revert !important;
  border-color: revert !important;
  color: revert !important;
  cursor: revert !important;
  pointer-events: revert !important;
  }

}


/* Search Pagination */

/* Fix: Pagination Next and Prev still show hover state when "disabled". This gives false impression that it is interactive. */

.pagination {

  .prev, .next {

    &.disabled {
    background-color: transparent !important;
    background-image: none !important;
    border-color: transparent !important;
    color: #ccc !important;
    cursor: pointer !important; /* Probably do not need this, since href being removed */
    pointer-events: none !important;

      &::after, &::before {
      color: inherit !important;
      } 

    }

  }

}

/* Fix: We appear to be removing the up and down arrow on input[number] because we do not like the way it looks, but this prevents users from understanding how to use the input. The visual que is very important here. */

.pagination-current {
max-inline-size: fit-content !important;
appearance: auto !important;
-moz-appearance: auto !important;
width: auto !important;

  &::-webkit-outer-spin-button, &::-webkit-inner-spin-button {

     -webkit-appearance: auto !important;

  }

}

Release Notes Anchor (Release Notes)

Keep up to date on major changes to this component.

Feature
June 20, 2026: Language support now added! Switched to thee point version and bumped up to 2.0.4

Note
Issues immediately corrected by the script when any Career Site page loads are considered "Static." Those corrected both on page load and during DOM updates, such as Ajax changes, are "Dynamic."

Feature
Open the majority of these issues into JIRA in 2027! (High)

Report Issues

Find a bug? Want a new feature? Report it on JIRA. You're so awesome!