Skip to content

Instantly share code, notes, and snippets.

@derekcavaliero
Last active June 16, 2023 13:34
Show Gist options
  • Save derekcavaliero/1d16bc7f02357087e179aa19f8c7ce08 to your computer and use it in GitHub Desktop.
Save derekcavaliero/1d16bc7f02357087e179aa19f8c7ce08 to your computer and use it in GitHub Desktop.
HubSpot Embedded Form Accessibility Polyfills
/**
* HubSpot Embedded Form Accessibility Pollyfills
*
* This script fixes the HubSpot HTML blunders that make their embedded forms inaccessible for assistive technology.
* - Replaces/removes improper use of <fieldset>, <legend>, <label>, and role attributes.
* - Note - this can cause forms configured for mulitple column field layouts to break - you will need to adjust your CSS accordingly.
**/
hubspotFormA11y = {
changeTag: function(node, tag) {
const clone = document.createElement(tag);
for (const attr of node.attributes) {
clone.setAttributeNS(null, attr.name, attr.value);
}
while (node.firstChild) {
clone.appendChild(node.firstChild);
}
node.replaceWith(clone);
return clone;
},
polyfillInitialHtml: function(form) {
form.querySelectorAll('fieldset, legend').forEach(element => {
this.changeTag(element, 'div');
});
form.querySelectorAll('.hs-fieldtype-checkbox, .hs-fieldtype-radio').forEach(element => {
element.querySelectorAll('[role]').forEach(item => {
item.removeAttribute('role');
});
this.changeTag(element, 'fieldset');
});
form.querySelectorAll('.hs-fieldtype-checkbox > label, .hs-fieldtype-radio > label').forEach(element => {
this.changeTag(element, 'legend');
});
},
polyfillErrorMessages: function(fieldRoot) {
let errorRoot = fieldRoot.querySelector('.hs-error-msgs');
let errors = errorRoot.querySelectorAll('.hs-error-msg');
errorRoot.removeAttribute('role');
let input = fieldRoot.querySelector('.hs-input');
errorRoot.id = input.name + '-error-msgs';
input.setAttribute('aria-describedby', input.name + '-error-msgs');
errors.forEach(error => {
let label = fieldRoot.querySelector('label[for="' + input.id + '"]');
if (error.innerText == 'Please complete this required field.')
error.innerText = 'The "' + label.innerText.replace('*', '') + '" field is required.';
this.changeTag(error, 'span');
});
},
untetherErrorMessages: function(fieldRoot) {
let input = fieldRoot.querySelector('.hs-input');
input.removeAttribute('aria-describedby');
},
observer: function(form) {
const observer = new MutationObserver((mutationList, observer) => {
for (const mutation of mutationList) {
if (mutation.type !== 'childList')
return;
if (mutation.addedNodes.length) {
let node = mutation.addedNodes[0];
if (node.nodeType === 3) // text node early return
return;
// console.log('Node(s) added', mutation.addedNodes, mutation.target);
if (node.matches('.hs-error-msgs'))
this.polyfillErrorMessages(mutation.target);
}
if (mutation.removedNodes.length) {
let node = mutation.removedNodes[0];
if (node.nodeType === 3) // text node early return
return;
// console.log('Node(s) removed', mutation.removedNodes, mutation.target);
if (node.hasOwnProperty('matches') && !node.matches('.hs-error-msgs'))
return;
this.untetherErrorMessages(mutation.target);
}
}
});
observer.observe(form, {
attributes: false,
childList: true,
subtree: true
});
}
};
window.addEventListener('message', msg => {
if (msg.data.type !== 'hsFormCallback')
return;
const formId = msg.data.id;
const form = document.querySelector('form[data-form-id="' + formId + '"]');
switch(msg.data.eventName) {
case 'onFormReady':
hubspotFormA11y.polyfillInitialHtml(form);
hubspotFormA11y.observer(form);
break;
case 'onFormFailedValidation':
break;
case 'onFormSubmit':
break;
case 'onFormSubmitted':
break;
}
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment