@zohodesk/a11y
Version:
In this Package, We Provide Some Basic Components For Accessibility.
378 lines (332 loc) • 12.6 kB
JavaScript
import { useEffect, useRef } from 'react';
import { preventDOMAvailabllityAndFocus, getNextEle, getPrevEle, listeners, getFocusArr, getTabArr, getListArr, isHiddenElement, isTabKeyEnabled, applyListStyle, applyFocusStyle, removeFocusStyle, removeListStyle, isListEle, customAttributes, getScrollContainer, detectListAndClear, clearFocusStorage } from "../utils/focusScopeUtil";
import { newRestoreEle } from "./useRestoreFocus";
import { setA11yLibraryConfig } from "../../Provider/Config";
import useEvent from "../../hooks/useEvent";
const {
focusContainer: focusContainerEle,
listFocused,
focusScope
} = customAttributes;
const checkActiveElementInRightContentRef = (activeElement, contentRef) => {
if (!contentRef().contains(activeElement)) {
return false;
}
let nestNodes = contentRef().querySelectorAll(`[${focusScope}]`);
let isThisSomebodyIsActiveElement = Array.from(nestNodes).some(el => el.contains(activeElement));
if (isThisSomebodyIsActiveElement) {
return false;
}
return true;
};
export default (_ref => {
let {
contentRef,
needListNavigation = false,
needFocusLoop = false,
onFocus,
onClose,
needEnterAction,
loadNextOptions,
searchValue,
isFetchingOptions
} = _ref;
let isArrowEvent = useRef(false);
let selectedIndex = useRef(-1);
let nextOptionsLoaded = useRef(false);
let oldListArr = useRef([]);
let {
listArr: initialListArr,
firstListEle,
bottomListEle
} = getListArr(contentRef());
let scrollContainer = contentRef() && getScrollContainer(contentRef());
let prevObserverItem = useRef(null);
useEffect(() => {
nextOptionsLoaded.current = false;
selectedIndex.current = -1;
contentRef() && detectListAndClear(contentRef, firstListEle); // checking if there is any focusedListEle while everytime rendering focusScope. It will be helpful when listitems get modified. Ex: search case
}, [searchValue]);
const options = {
root: scrollContainer,
// using scrollable container of listelements as root.
threshold: 0.5 // Trigger the intersection callback when at least 50% of the target is visible
}; // observing last list element to load next options while navigating down the list container
const listObserver = new IntersectionObserver(entries => {
const bottomList = entries[0];
if (bottomList.isIntersecting) {
nextOptionsLoaded.current = true;
listObserver.unobserve(bottomList.target);
loadNextOptions();
}
}, options);
const listChanged = useEvent(observerItem => {
if (observerItem && loadNextOptions) {
prevObserverItem.current && listObserver.unobserve(prevObserverItem.current);
listObserver.observe(observerItem);
prevObserverItem.current = observerItem;
}
if (nextOptionsLoaded.current) {
nextOptionsLoaded.current = false;
} else if (true || initialListArr.length == 0) {
selectedIndex.current = -1;
}
});
if (initialListArr) {
if (oldListArr.current.length !== initialListArr.length) {
listChanged(bottomListEle);
oldListArr.current = initialListArr;
}
}
let tabLooping = useEvent(event => {
let activeElement = document.activeElement;
if (!checkActiveElementInRightContentRef(activeElement, contentRef)) {
return;
}
let {
focusArr,
firstEle,
lastEle
} = getFocusArr(contentRef());
let {
listArr,
bottomListEle: arrowBottomListEle
} = getListArr(contentRef());
let {
tabArr,
firstTabEle,
lastTabEle
} = getTabArr(contentRef());
let focusContainer = contentRef().closest(`[${focusContainerEle}=true]`) || contentRef(); //animation
let focusedListEle = contentRef().querySelector(`[${listFocused} = true]`); // if list elements get modified, we should modify the selectedIndex. Ex: search case
if (oldListArr.current.length !== listArr.length) {
oldListArr.current = listArr;
initialListArr = listArr;
listChanged(arrowBottomListEle);
} // esc key
if (event.keyCode == 27) {
preventDOMAvailabllityAndFocus(newRestoreEle.el, onFocus);
onClose && onClose();
applyFocusStyle({
ele: newRestoreEle.el
});
} // enter key
if (needEnterAction && event.keyCode == 13) {
if (event.target && event.target.tagName.toUpperCase() !== 'TEXTAREA' && !event.target.isContentEditable) {
event.preventDefault();
} // event.stopPropagation();
if (focusedListEle) {
if (selectedIndex.current > -1) {
event && event.stopPropagation(); // to prevent parent enter action if any list is active.
listArr[selectedIndex.current].click();
}
} else {
if (document.querySelectorAll(`[data-a11y-list-active=true]`).length === 0 && event.target.click) {
event.target.click();
}
}
} //for Switch Tabs
// if(isSwitchTab){
if (event.key == 'ArrowLeft' || event.key == 'ArrowRight') {
// event.preventDefault();
event.stopPropagation();
let currentIndex = tabArr.indexOf(activeElement);
if (tabArr.includes(activeElement)) {
if (event.key == 'ArrowRight') {
let nextEle = activeElement == lastTabEle ? firstTabEle : tabArr[currentIndex + 1];
preventDOMAvailabllityAndFocus(nextEle, onFocus);
} else if (event.key == 'ArrowLeft') {
let previousEle = activeElement == firstTabEle ? lastTabEle : tabArr[currentIndex - 1];
preventDOMAvailabllityAndFocus(previousEle, onFocus);
}
}
} // }
//for ArrowControls
if (needListNavigation) {
if (event.key === 'ArrowUp' || event.key === 'ArrowDown') {
event.stopPropagation();
event.preventDefault();
isArrowEvent.current = true;
let prevListEle = listArr[selectedIndex.current];
let currentIndex = listArr.includes(activeElement) ? listArr.indexOf(activeElement) : selectedIndex.current; // what if auto focus on the list element.
if (isTabKeyEnabled() && focusContainer !== contentRef()) {
removeFocusStyle({
ele: focusContainer
});
}
if (!isFetchingOptions) {
if (event.key === 'ArrowDown') {
if (isTabKeyEnabled() && !listArr.includes(activeElement)) {
// when pressing down arrow, what if current activeElement isn't a list element.
let currentIndex = focusArr.includes(activeElement) ? focusArr.indexOf(activeElement) : 0;
let nextEle = getNextEle({
listArr,
focusArr,
index: currentIndex
});
selectedIndex.current = nextEle && listArr.indexOf(nextEle);
} else {
currentIndex == listArr.length - 1 ? selectedIndex.current = 0 : selectedIndex.current = currentIndex + 1;
}
} else if (event.key === 'ArrowUp') {
if (isTabKeyEnabled() && !listArr.includes(activeElement)) {
// swhen pressing up arrow, what if current activeElement isn't a list element.
let currentIndex = focusArr.includes(activeElement) ? focusArr.indexOf(activeElement) : focusArr.length - 1;
let prevEle = getPrevEle({
listArr,
focusArr,
index: currentIndex
});
selectedIndex.current = prevEle && listArr.indexOf(prevEle);
} else {
currentIndex == -1 || currentIndex == 0 ? selectedIndex.current = listArr.length - 1 : selectedIndex.current = currentIndex - 1;
}
}
applyListStyle({
element: listArr[selectedIndex.current],
shiftFocus: isTabKeyEnabled() || listArr.includes(activeElement),
prevListEle,
onFocus
});
}
}
} //For TabControls
if (event.key === 'Tab' || event.keyCode === 9 && event.shiftKey) {
let previousEle = focusedListEle ? focusArr[focusArr.indexOf(focusedListEle) - 1] : focusArr[focusArr.indexOf(activeElement) - 1];
let nextEle = focusedListEle ? focusArr[focusArr.indexOf(focusedListEle) + 1] : focusArr[focusArr.indexOf(activeElement) + 1];
setA11yLibraryConfig({
tabKeyEnabled: true
});
if (isTabKeyEnabled() && focusContainer !== contentRef()) {
removeFocusStyle({
ele: focusContainer
});
}
if (event.key === 'Tab' && !event.shiftKey) {
if (activeElement == lastEle || focusedListEle == lastEle) {
if (needFocusLoop) {
isHiddenElement({
ele: firstEle,
shiftFocus: true,
onFocus
});
} else {
requestAnimationFrame(() => {
onClose && onClose();
isHiddenElement({
ele: document.activeElement,
shiftFocus: true,
onFocus
});
});
}
} else {
if (isListEle(nextEle)) {
selectedIndex.current = selectedIndex.current + 1;
}
isHiddenElement({
ele: nextEle,
shiftFocus: focusedListEle && true,
onFocus
});
}
} else if (event.key === 'Tab' && event.shiftKey) {
if (activeElement == firstEle || (focusedListEle ? focusedListEle == firstEle : activeElement == contentRef())) {
if (needFocusLoop && activeElement == firstEle) {
isHiddenElement({
ele: lastEle,
shiftFocus: true,
onFocus
});
} else {
requestAnimationFrame(() => {
onClose && onClose();
isHiddenElement({
ele: document.activeElement,
shiftFocus: true,
onFocus
});
applyFocusStyle({
ele: document.activeElement
});
});
}
} else {
if (isListEle(previousEle)) {
selectedIndex.current = selectedIndex.current - 1;
}
isHiddenElement({
ele: previousEle,
shiftFocus: focusedListEle && true,
onFocus
});
}
}
focusedListEle && removeListStyle({
ele: focusedListEle
});
}
}); //To listen the focus, whenever happens within a scope
const onEleFocus = useEvent(event => {
applyFocusStyle({
ele: event.target
});
if (isListEle(event.target)) {
if (selectedIndex.current === -1) {
selectedIndex.current = initialListArr.indexOf(event.target);
}
applyListStyle({
element: event.target
});
}
onFocus && onFocus(event);
}); //To listen the blur, whenever happens within a scope
const onEleBlur = useEvent(event => {
removeFocusStyle({
ele: event.target
});
isListEle(event.target) && removeListStyle({
ele: event.target
});
}); //To prevent focus style when focus happens using mouse action
const onEleMouseDown = useEvent(event => {
isArrowEvent.current = false;
clearFocusStorage();
onEleBlur(event);
}); // const isListenerAdded = contentRef() && contentRef().getAttribute('data-a11y-focus-listener') == 'true';
// const onFocusOut = useEvent((event) => {
// if(!contentRef().contains(event.relatedTarget)) {
// // contentRef().setAttribute('data-a11y-focus-listener', false);
// // clearTabLoop();
// }
// })
//Adding Event Listeners to the FocusScope
let createTabLoop = useEvent(() => {
listeners({
element: contentRef(),
listenerName: 'add',
eventList: ['keydown', 'focus', 'blur', 'mousedown'],
actions: [tabLooping, onEleFocus, onEleBlur, onEleMouseDown],
isBubbling: true
}); // contentRef() && contentRef().addEventListener('focusout', onFocusOut);
}); //Removing Event Listeners to the FocusScope
let clearTabLoop = useEvent(() => {
listeners({
element: contentRef(),
listenerName: 'remove',
eventList: ['keydown', 'focus', 'blur', 'mousedown'],
actions: [tabLooping, onEleFocus, onEleBlur, onEleMouseDown],
isBubbling: true
});
isArrowEvent.current = false;
selectedIndex.current = -1;
});
useEffect(() => {
if (needListNavigation || needFocusLoop) {
createTabLoop();
} else {
clearTabLoop();
}
return clearTabLoop;
}, [needListNavigation, needFocusLoop]);
});