UNPKG

@zohodesk/a11y

Version:

In this Package, We Provide Some Basic Components For Accessibility.

434 lines (406 loc) 14.6 kB
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); }); } };