Prologue
A while ago, I decided to develop a fully accessible main navigation component in React and write a series of articles documenting the steps it took to create a non-trivial accessible component.
My last development with releases tied to one or more articles; each builds on the previous one until a fully implemented navigation component is complete.
Each release and its associated tag contain fully runnable code for the article. The code discussed in this article is available in the release. and may be downloaded at along with previous requirements.
Content Links
- Introduction
- Acceptance Criteria
Entering
Closings
- Setting Up For Success
Introduction
The implementation of keyboard handling left one obvious keyboard issue to fix: an apparent keyboard trap that occurs when focus shifts into the component via a Shift+Tab key combination.
In addition to the keyboard trap, other issues arise that, while not errors, can be refined to meet user expectations. Closing sublists when a parent is closed, or closing open lists when navigating through the top row, will give the component a finished aspect.
This release, while fixing one nagging issue, once again focuses on the internal, foundational aspects necessary to support those requirements and sets up the next release for success, enhancing the component to close cleanly, beginning with additions to the NavigationProvider.
Acceptance Criteria
Focus and Closing Foundation Release
- AC 1 - When Shift+Tab is used to enter the component and the last child on the top row is a button, focus should land on that button
A navigation component is not modal; it does not block interaction with other elements on a page, and exiting can be done using the Tab and Shift+Tab keys on the first and last component elements.
As a reminder, the entire component is always in the document object model, meaning that when Shift+Tab is executed to shift focus into the component, there's a good chance the focus will initially land on a link in an unexpanded sublist. In that case, the focus should shift to its parent on the last row.
For the last component element, if it is contained in an unexpanded sublist, focus should shift to its parent on the last row.
Entering
AC 1 fixes the last remaining keyboard-related issue, which occurs when Shift+Tab moves focus from outside the component to the last focusable element in the navigation component. If the top-row parent is a button and the sublist containing the last focusable element is closed, focus should shift to the top-row parent.
Any disappearance of the focus outline is regarded as an error by a screen-perceiving, keyboard-operating user.
The solution is to shift the focus to the top-row parent when focus is placed on the last element of the component within a closed sublist. This requires another event listener, onFocus.
Navigation Item
const handleFocus = () => {
const linkEl = linkRef.current;
const focusableEl = handleLinkFocus(linkEl);
if (!!focusableEl && focusableEl !== linkEl) {
shiftFocus(focusableEl);
}
};
...
const linkProps = {
...,
onFocus: handleFocus,
onKeyDown: handleKeyDown,
ref: linkRef,
...rest,
};
GitHub (release 0.8.0) -
The last element of a component is always a link, so a check is implemented to determine whether the link holding focus is the last component element; if so, the handler for the last child is called, which will either return itself or its topmost ancestor on the top row.
_isLastElementInComponent
const _isLastElementInComponent = (focusedEl) => {
return focusedEl === _getLastElementbyComponent();
};
GitHub (release 0.8.0) -
Determining the last element of the component involves finding the last focusable element associated with a particular parent, which, in this case, would be the last element in the top row. The _getLastElementByParent function triggers a recursive call, which can be resource-intensive. Since the entire nested list resides in the DOM, the last element will not change, so it makes sense to store it in state on the first call.
If the link is the last element within the component, a specific handler for that condition is called.
_handleLastChildFocus
const _handleLastChildFocus = (focusedEl) => {
const { isSubListOpen } = _getNavigationObjectByListElement(focusedEl);
if (!isSubListOpen) {
return _getLastElementInTopRow(focusedEl);
} else {
return focusedEl;
}
};
GitHub (release 0.8.0) -
If the last element on the top row is a button, the recursive function is called to request the last component element associated with the last element in the top row; if the link matches, the last element on the top row is returned, and the issue is fixed.
Retrieving the last element in the top row first requires fetching the last element in that row.
_getLastElementInIndexedList
const _getLastElementInIndexedList = useCallback(
(index) => {
const { storedList } = getNavigationArray()[index];
return storedList[storedList.length - 1];
},
[getNavigationArray],
);
GitHub (release 0.8.0) -
Unlike updates to state, updates to a ref held within the context provider don't trigger a re-render when the map object changes.
getNavigationArray
const getNavigationArray = useCallback(() => {
return state.navigationArray.map((obj) => ({
...obj,
dispatchSubListClose: _dispatchSubListCloseByParent.current.get(
obj.storedParentEl,
),
}));
}, [state.navigationArray]);
GitHub (release 0.8.0) -
RegisterButtonAsParent is also modified, setting the map directly and tying the close function to the parent element.
All that's left to do is to add the close function in the correct useEffect.
SubNavigation
useEffect(() => {
if (buttonRef.current !== null) {
registerItemInCurrentList(buttonRef.current as FocusableElementType);
registerButtonAsParent(
isSubListOpen,
buttonRef.current,
closeSubNavigation,
);
}
}, [buttonRef, closeSubNavigation, isSubListOpen, registerButtonAsParent, registerItemInCurrentList]);
GitHub (release 0.8.0) - SubNavigation.tsx - Line 83
With registration complete, work on actually closing the sublists can begin and will be detailed in the next article.
Summary
With keyboard navigation (finally) completed, attention now turns to refinements; fixing the last entry issue by sending focus to the last button in the top row when the last element in the component receives focus in an unopened list and preparing architecture to support each button closing itself and any open sublists maintained by its children.
SOCIAL SHARE CARD GENERATOR