@zohodesk/a11y
Version:
In this Package, We Provide Some Basic Components For Accessibility.
434 lines (406 loc) • 14.6 kB
JavaScript
import { getA11yLibraryConfig, setA11yLibraryConfig } from '../../Provider/Config';
import style from '../FocusScope.module.css';
const focusableElements = ["input:not([disabled]):not([type=hidden])", "textarea:not([disabled])", "button:not([disabled])", "select", "a[href]", "iframe", '[tabindex]:not([tabindex="-1"])'];
const TAB_ELEMENT = focusableElements.join(':not([tabindex="-1"]),');
export const customAttributes = {
customFocusStyle: 'data-a11y-focus-class',
// value:string(css class) => custom focus style attribute
customListStyle: 'data-a11y-list-focus-class',
//value: string(css class) => custom list focus style attribute
insetFocus: 'data-a11y-inset-focus',
// value: boolean => if we need inset focus style, we can set this attribute as true
needFocusStyle: 'data-a11y-need-focus-style',
//value: boolean => if we don't need focus style for particular element, we could use this attr
focusContainer: 'data-a11y-focus-container',
//value: boolean => if we need some other container element to be the focus container, we could use this attr
scrollContainer: 'data-scroll',
// value: boolean => To detect the scrollable container of focusable elements
autoFocus: 'data-a11y-autofocus',
// value: boolean => To detect the auto focusable element.
// for internal usage
listFocused: 'data-a11y-list-focused',
//value: boolean => To track the fake focus list elements, helps for switching arrow to tab focus (track)
focusScope: 'data-a11y-focus-scope',
// value: boolean => This attribute will be added to the first container of focus scope. so that we could able to track current element placed in right scope.
landmarkLabel: 'data-a11y-landmark-label' // value: string => To get the landmark label of an element which will be used to 'Skip navigation'.
};
const {
customFocusStyle,
customListStyle,
insetFocus,
listFocused,
needFocusStyle,
scrollContainer,
autoFocus,
landmarkLabel,
focusScope
} = customAttributes;
const elements = {
listElements: '[role=option], [role=menuitem], [data-a11y-list=true]',
tabElements: '[role=tab]',
landmarkElements: {
'header': {
role: 'banner',
ancestors: ['article', 'aside', 'main', 'nav', 'section:not([aria-label]):not([aria-labelledby])', '[role=complementary]', '[role=main]', '[role=navigation]', '[role=region]:not([aria-label]):not([aria-labelledby])']
},
'nav': {
role: 'navigation'
},
'main': {
role: 'main'
},
'section': {
role: 'region',
attributes: ['aria-label', 'aria-labelledby']
},
'aside': {
role: 'complementary'
},
'footer': {
role: 'contentinfo',
ancestors: ['article', 'aside', 'main', 'nav', 'section:not([aria-label]):not([aria-labelledby])', '[role=complementary]', '[role=main]', '[role=navigation]', '[role=region]:not([aria-label]):not([aria-labelledby])']
},
'form': {
role: 'form',
attributes: ['aria-label', 'aria-labelledby']
}
}
};
const {
listElements,
tabElements,
landmarkElements
} = elements;
export const landmarkElementsObj = {
NAV: 'Navigation',
HEADER: 'Banner',
ASIDE: 'Complementary',
MAIN: 'Main',
SECTION: 'Region',
FOOTER: 'contentinfo',
FORM: 'Form'
};
const findMainElement = elements => {
for (let [i, element] of elements.entries()) {
if (element.tagName === 'MAIN') {
let mainElement = elements.splice(i, 1)[0];
elements.unshift(mainElement);
return {
elements,
mainElement
};
}
}
return {
elements
};
}; // export const getLandmarkElements = ( element ) => {
// if (element) {
// let elements = [];
// Object.keys(landmarkElements).forEach((ele) => {
// let eleObj = landmarkElements[ele];
// if(eleObj.attributes) {
// eleObj.attributes.forEach((attribute) => {
// elements.push(`${ele}[${attribute}]`, `[role=${eleObj.role}][${attribute}]`)
// });
// } else if(eleObj.ancestors) {
// let eleSelector = `${ele}`;
// let roleSelector = `[role=${eleObj.role}]`;
// eleObj.ancestors.forEach((ancestor) => {
// eleSelector = eleSelector + `:not(${ancestor} ${ele})`;
// roleSelector = roleSelector + `:not(${ancestor} [role=${eleObj.role}])`
// });
// elements.push(eleSelector, roleSelector);
// } else {
// elements.push(ele, `[role=${eleObj.role}]`)
// }
// });
// let filteredElements = elements.map(ele => ele + ':not([data-a11y-skip-landmark=true])');
// filteredElements = element && Object.values(element.querySelectorAll(filteredElements));
// const elementsObj = findMainElement(filteredElements);
// return elementsObj || {};
// }
// }
// export const getLandmarkElements = element => {
// if(element) {
// let elements = [];
// Object.keys(landmarkElements).forEach(ele => {
// let eleObj = landmarkElements[ele];
// elements.push(`${ele}[data-a11y-landmark-label]`, `[role=${eleObj.role}][data-a11y-landmark-label]` )
// });
// let filteredElements = elements.map(ele => ele + ':not([data-a11y-skip-landmark=true])');
// filteredElements = element && Object.values(element.querySelectorAll(filteredElements));
// const elementsObj = findMainElement(filteredElements);
// return elementsObj || {};
// }
// }
export const getLandmarkSelectors = () => {
let selectors = [];
Object.keys(landmarkElements).forEach(ele => {
let eleObj = landmarkElements[ele];
selectors.push(`${ele}[data-a11y-landmark-label]:not([data-a11y-skip-landmark=true])`, `[role=${eleObj.role}][data-a11y-landmark-label]:not([data-a11y-skip-landmark=true])`);
});
return selectors;
};
export const getLandmarkElements = element => {
if (element) {
let landmarkSelectors = getLandmarkSelectors();
let selectors = [...landmarkSelectors, 'iframe'];
let documentLandmarks = element.querySelectorAll(selectors);
let allLandmarkElements = [];
documentLandmarks.forEach(ele => {
if (!isElementHiddenInTree(ele)) {
if (ele.tagName === 'IFRAME') {
let iframeDocument = ele.contentDocument && ele.contentDocument.body || ele.contenWindow && ele.contentWindow.document;
if (iframeDocument) {
let iframeLandmarkElements = iframeDocument.querySelectorAll(landmarkSelectors);
iframeLandmarkElements.forEach(eleInIframe => {
if (!isElementHiddenInTree(eleInIframe)) {
allLandmarkElements.push(eleInIframe);
}
});
}
} else {
allLandmarkElements.push(ele);
}
}
});
const elementsObj = findMainElement(allLandmarkElements);
return elementsObj || {};
}
};
export const isElementHidden = element => {
return getComputedStyle(element).visibility === 'hidden' || getComputedStyle(element).opacity === '0';
};
export const isElementHiddenInTree = element => {
if (element) {
let current = element;
while (current) {
if (isElementHidden(element)) {
return true;
}
if (current.parentElement) {
current = current.parentElement;
} else {
return false;
}
}
return false;
}
};
export const getEleName = ele => {
const name = ele.getAttribute('role') ? ele.getAttribute('role') : landmarkElementsObj[ele.tagName];
if (ele.getAttribute(landmarkLabel) !== null) {
return ele.getAttribute(landmarkLabel);
} else {
if (ele.getAttribute('aria-label') !== null) {
return ele.getAttribute('aria-label') + ' ' + name;
} else {
return name;
}
}
};
export const clearFocusStorage = () => {
setA11yLibraryConfig({
tabKeyEnabled: false
});
};
export const preventDOMAvailabllityAndFocus = function (elRef, onFocus) {
let preventScroll = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
requestAnimationFrame(() => {
elRef && elRef.focus({
preventScroll
});
onFocus && onFocus(elRef);
});
};
const getAutoFocusEle = elements => {
const autoFocusEle = elements.find(ele => ele.getAttribute(autoFocus) === 'true');
return autoFocusEle ? autoFocusEle : null;
};
export const getScrollContainer = container => {
return container.querySelectorAll(`[${scrollContainer}=true]`)[0] || container;
};
export const isTabKeyEnabled = () => {
return getA11yLibraryConfig('tabKeyEnabled');
};
export const isListEle = ele => {
return ele.getAttribute('role') === 'option' || ele.getAttribute('role') === 'menuitem' || ele.getAttribute('data-a11y-list') === 'true';
};
export const isHiddenElement = _ref => {
let {
ele,
shiftFocus,
onFocus
} = _ref;
if (ele && getComputedStyle(ele).visibility == 'hidden') {
ele.classList.add(style.focusVisible);
preventDOMAvailabllityAndFocus(ele, onFocus);
} else if (ele && getComputedStyle(ele).opacity == 0) {
ele.classList.add(style.focusOpacity);
preventDOMAvailabllityAndFocus(ele, onFocus);
}
if (shiftFocus) {
preventDOMAvailabllityAndFocus(ele, onFocus);
}
};
export const getFocusArr = contentRef => {
let focusArr = contentRef && contentRef.querySelectorAll(TAB_ELEMENT); // All focusable Elements
focusArr = focusArr && Object.values(focusArr);
let firstEle = focusArr && focusArr[0];
let lastEle = focusArr && focusArr[focusArr.length - 1];
let autoFocusEle = focusArr && getAutoFocusEle(focusArr);
return {
focusArr,
firstEle,
lastEle,
autoFocusEle
};
};
export const getListArr = contentRef => {
let listArr = contentRef && contentRef.querySelectorAll(listElements); // list elements
listArr = listArr && Object.values(listArr);
var firstListEle = listArr && listArr[0];
var lastListEle = listArr && listArr[listArr.length - 1];
var bottomListEle = listArr && listArr[listArr.length - 3];
return {
listArr,
firstListEle,
lastListEle,
bottomListEle
};
};
export const getTabArr = contentRef => {
let tabArr = contentRef && contentRef.querySelectorAll(tabElements);
tabArr = tabArr && Object.values(tabArr);
let firstTabEle = tabArr && tabArr[0];
let lastTabEle = tabArr && tabArr[tabArr.length - 1];
return {
tabArr,
firstTabEle,
lastTabEle
};
};
export const getPrevEle = _ref2 => {
let {
listArr,
focusArr,
index
} = _ref2;
let traverseArr = focusArr.slice(0, index);
const prevListEle = traverseArr.length && traverseArr.find((element, index, array) => index === array.length - 1 && isListEle(element));
return prevListEle ? prevListEle : listArr[listArr.length - 1];
};
export const getNextEle = _ref3 => {
let {
listArr,
focusArr,
index
} = _ref3;
let traverseArr = focusArr.slice(index);
const nextListEle = traverseArr.length && traverseArr.find(ele => isListEle(ele));
return nextListEle ? nextListEle : listArr[0];
};
export const removeListStyle = _ref4 => {
let {
ele
} = _ref4;
if (ele) {
const listStyle = getA11yLibraryConfig('globalListFocusClass') || style.listFocusStyle;
if (ele.getAttribute(customListStyle)) {
ele.classList.remove(ele.getAttribute(customListStyle));
} else {
ele.classList.remove(listStyle);
}
ele.setAttribute(listFocused, 'false');
}
};
export const applyListStyle = _ref5 => {
let {
element,
shiftFocus,
prevListEle,
onFocus
} = _ref5;
if (element) {
if (shiftFocus) {
preventDOMAvailabllityAndFocus(element, onFocus);
} else {
const listStyle = getA11yLibraryConfig('globalListFocusClass') || style.listFocusStyle;
if (element.getAttribute(customListStyle)) {
element.classList.add(element.getAttribute(customListStyle));
} else {
element.classList.add(listStyle);
}
element.setAttribute(listFocused, 'true');
element.scrollIntoView({
behavior: 'auto',
block: 'nearest'
});
}
}
if (prevListEle && element !== prevListEle) {
removeListStyle({
ele: prevListEle
});
}
};
export const applyFocusStyle = _ref6 => {
let {
ele
} = _ref6;
if (ele) {
const focusStyle = getA11yLibraryConfig('globalFocusClass') || style.focusStyle;
const isFocusRingEnabled = getA11yLibraryConfig('focusRing') && (getA11yLibraryConfig('focusRingEnabledPlaces') === 'all' || getA11yLibraryConfig('focusRingEnabledPlaces') === 'main' && (ele.closest('[data-a11y-focus-main-area=true]') || ele.getAttribute('data-a11y-focus-main-area') === true));
const isApplyStyle = ele.getAttribute(needFocusStyle) === 'true' || ele.getAttribute(needFocusStyle) !== 'false' && isFocusRingEnabled && isTabKeyEnabled(); // what if we don't need focusStyle for some ares. Ex: Dialog container
if (isApplyStyle) {
if (ele.getAttribute(customFocusStyle)) {
ele.classList.add(ele.getAttribute(customFocusStyle));
} else {
ele.classList.add(focusStyle);
if (ele.getAttribute(insetFocus) == 'true') {
ele.classList.add(style.insetFocus);
}
}
}
}
};
export const removeFocusStyle = _ref7 => {
let {
ele
} = _ref7;
let eleClassList = [style.insetFocus, style.focusStyle, getA11yLibraryConfig('globalFocusClass'), style.listFocusStyle, getA11yLibraryConfig('globalListFocusClass'), style.focusVisible, style.focusOpacity, ele.getAttribute(customFocusStyle), ele.getAttribute(customListStyle)];
eleClassList.map(style => {
if (ele.classList.contains(style)) {
ele.classList.remove(style);
}
});
};
export const detectListAndClear = (contentRef, firstListEle) => {
// checking if there is any focusedListEle while everytime rendering focusScope. It will be helpful when listitems get modified. Ex: search case
let initialFocusedListEle = contentRef().querySelector(`[${listFocused} = true]`);
if (initialFocusedListEle) {
initialFocusedListEle && removeListStyle({
ele: initialFocusedListEle
});
firstListEle.scrollIntoView({
behavior: 'auto',
block: 'nearest'
});
}
};
export const listeners = _ref8 => {
let {
element,
listenerName,
eventList = [],
actions = [],
isBubbling = false
} = _ref8;
let listenersType = listenerName == 'add' ? 'addEventListener' : 'removeEventListener';
if (element && eventList.length !== 0 && actions.length !== 0) {
eventList.map((event, index) => {
element[listenersType](event, actions[index], isBubbling);
});
}
};