UNPKG

@zohodesk/a11y

Version:

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

378 lines (332 loc) 12.6 kB
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]); });