Toc keyboard navigation (#4497)
* Adds keyboard navigation to table of content links Signed-off-by: Aaron Stephanus <taoist.futility@pm.me> * Adds table of contents scroll and item focus management to the current page or the first toc item Signed-off-by: Aaron Stephanus <taoist.futility@pm.me> * Add space key capture to the toc expand/collapse arrows Signed-off-by: Aaron Stephanus <taoist.futility@pm.me> * Adds escape key support for expand/collapse of toc categories on the category label Signed-off-by: Aaron Stephanus <taoist.futility@pm.me> * Adds missing aria-role and aria-current attributes to toc affordances Signed-off-by: Aaron Stephanus <taoist.futility@pm.me> * Changes the function of the space bar to navigate to a toc parent category item instead of only expand its children Signed-off-by: Aaron Stephanus <taoist.futility@pm.me> * Adds arrow up/down key support to toc navigation Signed-off-by: Aaron Stephanus <taoist.futility@pm.me> * Adds missing accessibility attribute handling to the toc Signed-off-by: Aaron Stephanus <taoist.futility@pm.me> * Corrects arrow key handling of keyboard navigation of the toc Signed-off-by: Aaron Stephanus <taoist.futility@pm.me> * Adds padding to allow for the keyboard focus rectangle Signed-off-by: Aaron Stephanus <taoist.futility@pm.me> * Removes explicit tabindex values for the toc navigation Signed-off-by: Aaron Stephanus <taoist.futility@pm.me> * Removes auto focus of toc navigation items on page load; refactors navigation panel scroll into view logic to account for the sticky version selector Signed-off-by: Aaron Stephanus <taoist.futility@pm.me> * Removes keyboard focus trap on search field for the tab key Signed-off-by: Aaron Stephanus <taoist.futility@pm.me> * Unifies navigation item focus state for top level items and sub navigation items Signed-off-by: Aaron Stephanus <taoist.futility@pm.me> * Removes the expand/collapse navigation buttons from being tab focusable Signed-off-by: Aaron Stephanus <taoist.futility@pm.me> * Corrects navigation item spacing to permit visible focus rectangles Signed-off-by: Aaron Stephanus <taoist.futility@pm.me> * Implements full arrow navigation for the toc items according to the w3c tree view navigation pattern Signed-off-by: Aaron Stephanus <taoist.futility@pm.me> * Adds reference link to the w3c documentation about the tree view navigation to the arrow key toc navigation javascript Signed-off-by: Aaron Stephanus <taoist.futility@pm.me> * Account for the difference between the navigation panel height, and the viewport height when auto scrolling the toc item of the current page Signed-off-by: Aaron Stephanus <taoist.futility@pm.me> * Corrects keyboard navigation focus rectangles from being slightly obscured on the bottom Signed-off-by: Aaron Stephanus <taoist.futility@pm.me> * Corrects aria and role attributes according to the navigation tree pattern as opposed to the disclosure pattern Signed-off-by: Aaron Stephanus <taoist.futility@pm.me> * Corrects Jekyll include parameter assignment syntax error Signed-off-by: Aaron Stephanus <taoist.futility@pm.me> * Removes aria-expanded setting from the templates since it is unrealiable to determine without iterating through the entire tree to determine if one of its children of children is the current page Signed-off-by: Aaron Stephanus <taoist.futility@pm.me> * Moves setting of aria-expanded attributes to a runtime behavior, and corrects arrow left/right behavior on navigation tree items Signed-off-by: Aaron Stephanus <taoist.futility@pm.me> * Ensures unique navigation menu element ids; conditionally sets aira-owns and aria-current attributes Signed-off-by: Aaron Stephanus <taoist.futility@pm.me> * Removes JavaScript that is unused following refactoring tree view navigation; adds comments explaining choices Signed-off-by: Aaron Stephanus <taoist.futility@pm.me> --------- Signed-off-by: Aaron Stephanus <taoist.futility@pm.me>
This commit is contained in:
parent
72d120f327
commit
80237e2e68
|
@ -1,4 +1,9 @@
|
||||||
<ul class="nav-list">
|
<ul
|
||||||
|
role="tree"
|
||||||
|
aria-expanded="{{ include.expanded | default: 'false' }}"
|
||||||
|
class="nav-list"
|
||||||
|
{%- if include.owned_tree_id -%}id="{{ include.owned_tree_id }}"{%- endif -%}
|
||||||
|
>
|
||||||
{%- assign titled_pages = include.pages
|
{%- assign titled_pages = include.pages
|
||||||
| where_exp:"item", "item.title != nil" -%}
|
| where_exp:"item", "item.title != nil" -%}
|
||||||
|
|
||||||
|
@ -60,28 +65,60 @@
|
||||||
{%- for node in pages_list -%}
|
{%- for node in pages_list -%}
|
||||||
{%- if node.parent == nil -%}
|
{%- if node.parent == nil -%}
|
||||||
{%- unless node.nav_exclude -%}
|
{%- unless node.nav_exclude -%}
|
||||||
<li class="nav-list-item{% if page.collection == include.key and page.url == node.url or page.parent == node.title or page.grand_parent == node.title %} active{% endif %}">
|
{% assign nested_owned_tree_id = include.owned_tree_id | append: "_" | append: forloop.index | append: "_" | append: node.title | append: "_navitems" | replace: " ", "_" %}
|
||||||
|
<li role="none" class="nav-list-item{% if page.collection == include.key and page.url == node.url or page.parent == node.title or page.grand_parent == node.title %} active{% endif %}">
|
||||||
{%- if node.has_children -%}
|
{%- if node.has_children -%}
|
||||||
<a href="#" class="nav-list-expander"><svg viewBox="0 0 24 24"><use xlink:href="#svg-arrow-right"></use></svg></a>
|
<a
|
||||||
|
role="treeitem"
|
||||||
|
aria-owns="{{ nested_owned_tree_id }}"
|
||||||
|
{%- if page.url == node.url -%}aria-current="page"{%- endif -%}
|
||||||
|
href="#"
|
||||||
|
class="nav-list-expander"
|
||||||
|
><svg viewBox="0 0 24 24"><use xlink:href="#svg-arrow-right"></use></svg></a>
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
<a href="{{ node.url | absolute_url }}" class="nav-list-link{% if page.url == node.url %} active{% endif %}">{{ node.title }}</a>
|
|
||||||
|
<a
|
||||||
|
role="treeitem"
|
||||||
|
{%- if node.has_children -%}aria-owns="{{ nested_owned_tree_id }}"{%- endif -%}
|
||||||
|
{%- if page.url == node.url -%}aria-current="page"{%- endif -%}
|
||||||
|
href="{{ node.url | absolute_url }}"
|
||||||
|
class="nav-list-link{% if page.url == node.url %} active{% endif %}"
|
||||||
|
>{{ node.title }}</a>
|
||||||
{%- if node.has_children -%}
|
{%- if node.has_children -%}
|
||||||
{%- assign children_list = pages_list | where: "parent", node.title -%}
|
{%- assign children_list = pages_list | where: "parent", node.title -%}
|
||||||
<ul class="nav-list ">
|
<ul role="tree" class="nav-list" id="{{ nested_owned_tree_id }}">
|
||||||
{%- for child in children_list -%}
|
{%- for child in children_list -%}
|
||||||
{%- unless child.nav_exclude -%}
|
{%- unless child.nav_exclude -%}
|
||||||
<li class="nav-list-item {% if page.url == child.url or page.parent == child.title %} active{% endif %}">
|
<li role="none" class="nav-list-item {% if page.url == child.url or page.parent == child.title %} active{% endif %}">
|
||||||
{%- if child.has_children -%}
|
{%- if child.has_children -%}
|
||||||
<a href="#" class="nav-list-expander"><svg viewBox="0 0 24 24"><use xlink:href="#svg-arrow-right"></use></svg></a>
|
{% assign nested_nested_owned_tree_id = nested_owned_tree_id | append: "_" | append: forloop.index | append: "_" | append: child.title | append: "_navitems" | replace: " ", "_" %}
|
||||||
|
<a
|
||||||
|
role="treeitem"
|
||||||
|
aria-owns="{{ nested_nested_owned_tree_id }}"
|
||||||
|
{%- if page.url == node.url -%}aria-current="page"{%- endif -%}
|
||||||
|
href="#"
|
||||||
|
class="nav-list-expander"
|
||||||
|
><svg viewBox="0 0 24 24"><use xlink:href="#svg-arrow-right"></use></svg></a>
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
<a href="{{ child.url | absolute_url }}" class="nav-list-link{% if page.url == child.url %} active{% endif %}">{{ child.title }}</a>
|
<a
|
||||||
|
role="treeitem"
|
||||||
|
{%- if child.has_children -%}aria-owns="{{ nested_nested_owned_tree_id }}"{%- endif -%}
|
||||||
|
{%- if page.url == node.url -%}aria-current="page"{%- endif -%}
|
||||||
|
href="{{ child.url | absolute_url }}"
|
||||||
|
class="nav-list-link{% if page.url == child.url %} active{% endif %}"
|
||||||
|
>{{ child.title }}</a>
|
||||||
{%- if child.has_children -%}
|
{%- if child.has_children -%}
|
||||||
{%- assign grand_children_list = pages_list | where: "parent", child.title | where: "grand_parent", node.title -%}
|
{%- assign grand_children_list = pages_list | where: "parent", child.title | where: "grand_parent", node.title -%}
|
||||||
<ul class="nav-list">
|
<ul role="tree" class="nav-list" id="{{ nested_nested_owned_tree_id }}">
|
||||||
{%- for grand_child in grand_children_list -%}
|
{%- for grand_child in grand_children_list -%}
|
||||||
{%- unless grand_child.nav_exclude -%}
|
{%- unless grand_child.nav_exclude -%}
|
||||||
<li class="nav-list-item {% if page.url == grand_child.url %} active{% endif %}">
|
<li role="none" class="nav-list-item {% if page.url == grand_child.url %} active{% endif %}">
|
||||||
<a href="{{ grand_child.url | absolute_url }}" class="nav-list-link{% if page.url == grand_child.url %} active{% endif %}">{{ grand_child.title }}</a>
|
<a
|
||||||
|
role="treeitem"
|
||||||
|
{%- if page.url == grand_child.url -%}aria-current="page"{%- endif -%}
|
||||||
|
href="{{ grand_child.url | absolute_url }}"
|
||||||
|
class="nav-list-link{% if page.url == grand_child.url %} active{% endif %}"
|
||||||
|
>{{ grand_child.title }}</a>
|
||||||
</li>
|
</li>
|
||||||
{%- endunless -%}
|
{%- endunless -%}
|
||||||
{%- endfor -%}
|
{%- endfor -%}
|
||||||
|
|
|
@ -66,7 +66,7 @@ layout: table_wrappers
|
||||||
| where_exp:"item", "item.nav_exclude != true"
|
| where_exp:"item", "item.nav_exclude != true"
|
||||||
| size %}
|
| size %}
|
||||||
{% if pages_top_size > 0 %}
|
{% if pages_top_size > 0 %}
|
||||||
{% include nav.html pages=site.html_pages key=nil %}
|
{% include nav.html pages=site.html_pages key=nil expanded=true %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if site.just_the_docs.collections %}
|
{% if site.just_the_docs.collections %}
|
||||||
{% assign collections_size = site.just_the_docs.collections | size %}
|
{% assign collections_size = site.just_the_docs.collections | size %}
|
||||||
|
@ -75,25 +75,49 @@ layout: table_wrappers
|
||||||
{% assign collection_value = collection_entry[1] %}
|
{% assign collection_value = collection_entry[1] %}
|
||||||
{% assign collection = site[collection_key] %}
|
{% assign collection = site[collection_key] %}
|
||||||
{% if collection_value.nav_exclude != true %}
|
{% if collection_value.nav_exclude != true %}
|
||||||
|
{% assign owned_tree_id = collection_key | append: "_" | append: forloop.index | append: "_navitems" | replace: " ", "_" %}
|
||||||
{% if collections_size > 1 or pages_top_size > 0 %}
|
{% if collections_size > 1 or pages_top_size > 0 %}
|
||||||
{% if collection_value.nav_fold == true %}
|
{% if collection_value.nav_fold == true %}
|
||||||
<ul class="nav-list nav-category-list">
|
<ul
|
||||||
<li class="nav-list-item{% if page.collection == collection_key %} active{% endif %}">
|
role="tree"
|
||||||
{%- if collection.size > 0 -%}
|
class="nav-list nav-category-list"
|
||||||
<a href="#" class="nav-list-expander"><svg viewBox="0 0 24 24"><use xlink:href="#svg-arrow-right"></use></svg></a>
|
>
|
||||||
{%- endif -%}
|
<li role="none" class="nav-list-item{% if page.collection == collection_key %} active{% endif %}">
|
||||||
|
|
||||||
{% assign collection_url_path = collection_key | append: "/index/" %}
|
{% assign collection_url_path = collection_key | append: "/index/" %}
|
||||||
<div class="nav-category"><a href="{{ collection_url_path | relative_url }}">{{ collection_value.name }}</a></div>
|
{% assign category_comparison_url = "/" | append: collection_url_path %}
|
||||||
{% include nav.html pages=collection key=collection_key %}
|
{%- if collection.size > 0 -%}
|
||||||
|
<a
|
||||||
|
role="treeitem"
|
||||||
|
aria-owns="{{ owned_tree_id }}"
|
||||||
|
aria-current="{% if page.collection == collection_key %}page{% endif %}"
|
||||||
|
href="#"
|
||||||
|
class="nav-list-expander"
|
||||||
|
><svg viewBox="0 0 24 24"><use xlink:href="#svg-arrow-right"></use></svg></a>
|
||||||
|
{%- endif -%}
|
||||||
|
<a
|
||||||
|
role="treeitem"
|
||||||
|
aria-owns="{{ owned_tree_id }}"
|
||||||
|
aria-current="{% if page.collection == collection_key %}page{% endif %}"
|
||||||
|
class="nav-category{% if category_comparison_url == page.url %} active{% endif %}"
|
||||||
|
href="{{ collection_url_path | relative_url }}"
|
||||||
|
>{{ collection_value.name }}</a>
|
||||||
|
{% include nav.html pages=collection key=collection_key owned_tree_id=owned_tree_id %}
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
{% else %}
|
{% else %}
|
||||||
{% assign collection_url_path = collection_key | append: "/index/" %}
|
{% assign collection_url_path = collection_key | append: "/index/" %}
|
||||||
<div class="nav-category"><a href="{{ collection_url_path | relative_url }}">{{ collection_value.name }}</a></div>
|
<a
|
||||||
{% include nav.html pages=collection key=collection_key %}
|
role="treeitem"
|
||||||
|
aria-owns="{{ owned_tree_id }}"
|
||||||
|
aria-current="{% if page.collection == collection_key %}page{% endif %}"
|
||||||
|
class="nav-category{% if category_comparison_url == page.url %} active{% endif %}"
|
||||||
|
href="{{ collection_url_path | relative_url }}"
|
||||||
|
>{{ collection_value.name }}</a>
|
||||||
|
{% include nav.html pages=collection key=collection_key owned_tree_id=owned_tree_id %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% else %}
|
{% else %}
|
||||||
{% include nav.html pages=collection key=collection_key %}
|
{% include nav.html pages=collection key=collection_key owned_tree_id=owned_tree_id %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
@ -111,7 +135,7 @@ layout: table_wrappers
|
||||||
{% assign docs_version = "latest" %}
|
{% assign docs_version = "latest" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<input type="text" id="search-input" class="search-input"
|
<input type="text" id="search-input" class="search-input"
|
||||||
tabindex="0" placeholder="Search..." aria-label="Search {{ site.title }}"
|
placeholder="Search..." aria-label="Search {{ site.title }}"
|
||||||
data-docs-version="{{ docs_version }}" autocomplete="off">
|
data-docs-version="{{ docs_version }}" autocomplete="off">
|
||||||
<div class="search-spinner"><i></i></div>
|
<div class="search-spinner"><i></i></div>
|
||||||
<label for="search-input" class="search-label"><svg viewBox="0 0 24 24" class="search-icon"><use xlink:href="#svg-search"></use></svg></label>
|
<label for="search-input" class="search-label"><svg viewBox="0 0 24 24" class="search-icon"><use xlink:href="#svg-search"></use></svg></label>
|
||||||
|
|
|
@ -54,10 +54,22 @@ code {
|
||||||
max-height: 100vh;
|
max-height: 100vh;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
min-width: 14rem;
|
min-width: 14rem;
|
||||||
|
padding-right: 1px;
|
||||||
|
padding-left: 1px;
|
||||||
|
padding-bottom: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav#site-nav > .nav-list:nth-of-type(1) {
|
||||||
|
padding-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-list {
|
||||||
|
margin-top: 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-category {
|
.nav-category {
|
||||||
text-align: start;
|
text-align: start;
|
||||||
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.main-content {
|
.main-content {
|
||||||
|
|
|
@ -1,27 +1,212 @@
|
||||||
let siteNav = document.querySelector('.site-nav');
|
|
||||||
const key = 'scroll';
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
const scroll = JSON.parse(sessionStorage.getItem(key));
|
const navParent = document.getElementById('site-nav');
|
||||||
|
if (!navParent) {
|
||||||
const currentDate = new Date();
|
return;
|
||||||
|
|
||||||
if (scroll !== null && currentDate.getTime() < scroll.expiry) {
|
|
||||||
siteNav.scrollTop = parseInt(scroll.value);
|
|
||||||
}
|
}
|
||||||
else {
|
|
||||||
sessionStorage.removeItem(key);
|
// The business logic on navigation items in layouts/default.html and _includes/nav.html
|
||||||
|
// is much too complex to reliably make this determination correctly without overly complicating
|
||||||
|
// something that is already overly complicated. So, this will make the corrections at runtime.
|
||||||
|
navParent.querySelectorAll('ul').forEach((element) => {
|
||||||
|
const hasNestedList = element.querySelector('ul');
|
||||||
|
if (hasNestedList) {
|
||||||
|
element.setAttribute('role', 'tree');
|
||||||
|
} else {
|
||||||
|
element.setAttribute('role', 'group');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
window.addEventListener('beforeunload', () => {
|
/**
|
||||||
const currentDate = new Date();
|
* This function configures the aria-expanded attributes on the navigation items.
|
||||||
|
* Liquid's limited features make this challenging to do cleanly at build time
|
||||||
// add the scroll value that expires after one day
|
* requiring searching forward through the tree to determine if something in the
|
||||||
const scroll = {
|
* depth of the currently rendering tree node has a descendant that is active.
|
||||||
value: siteNav.scrollTop,
|
*/
|
||||||
expiry: currentDate.getTime() + 24 * 60 * 60 * 1000,
|
function configureAriaAttributes() {
|
||||||
}
|
|
||||||
|
const setSubTreeAriaExpanded = (listItem, isExpanded) => {
|
||||||
sessionStorage.setItem(key, JSON.stringify(scroll));
|
const listItemAnchors = listItem.querySelectorAll('a');
|
||||||
|
listItemAnchors.forEach((element) => {
|
||||||
|
const parentLi = element.parentElement;
|
||||||
|
if (parentLi) {
|
||||||
|
const childUl = Array.from(parentLi.children).filter(element => element.tagName === 'UL')[0];
|
||||||
|
if (childUl) {
|
||||||
|
// If there is a child UL of the anchor element's parent LI then set the aria-expanded
|
||||||
|
// Otherwise delete the attribute, because there is no child UL to expand / collapse.
|
||||||
|
element.setAttribute('aria-expanded', isExpanded);
|
||||||
|
} else {
|
||||||
|
element.removeAttribute('aria-expanded');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const topLevelUls = Array.from(navParent.children).filter(element => element.tagName === 'UL');
|
||||||
|
topLevelUls.forEach((element) => {
|
||||||
|
const listItems = Array.from(element.children).filter(element => element.tagName === 'LI');
|
||||||
|
listItems.forEach((element) => {
|
||||||
|
const active = element.querySelector('a.active');
|
||||||
|
if (active) {
|
||||||
|
setSubTreeAriaExpanded(element, true);
|
||||||
|
} else {
|
||||||
|
setSubTreeAriaExpanded(element, false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Give keyboard focus to the active navigation item, and ensure
|
||||||
|
// it is scrolled into view.
|
||||||
|
const activeNavItem = navParent.querySelector('a.active');
|
||||||
|
if (activeNavItem) {
|
||||||
|
|
||||||
|
configureAriaAttributes();
|
||||||
|
|
||||||
|
// The active navigation item needs to have the tabindex="0" wheras all of the other items
|
||||||
|
// are excluded from the TAB order according.
|
||||||
|
activeNavItem.setAttribute('tabindex', '0');
|
||||||
|
navParent.querySelectorAll('a:not(.active)').forEach((element) => {
|
||||||
|
element.setAttribute('tabindex', '-1');
|
||||||
|
});
|
||||||
|
|
||||||
|
// If the active item is not in view, then scroll it into view.
|
||||||
|
const VERSION_WRAPPER_HEIGHT = 80;
|
||||||
|
const parentRect = navParent.getBoundingClientRect();
|
||||||
|
const activeRect = activeNavItem.getBoundingClientRect();
|
||||||
|
|
||||||
|
if (activeRect.top < (parentRect.top + VERSION_WRAPPER_HEIGHT)) {
|
||||||
|
const distanceToScroll = activeRect.top - parentRect.top - VERSION_WRAPPER_HEIGHT;
|
||||||
|
navParent.scrollTo(0, distanceToScroll);
|
||||||
|
} else if (activeRect.bottom > window.visualViewport.height) {
|
||||||
|
const distanceToScroll = activeRect.bottom - window.visualViewport.height + VERSION_WRAPPER_HEIGHT;
|
||||||
|
navParent.scrollTo(0, distanceToScroll);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
navParent.addEventListener('keydown', (event) => {
|
||||||
|
|
||||||
|
const handleSpaceKey = () => {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Space key functionality
|
||||||
|
// For reference: https://www.w3.org/WAI/ARIA/apg/patterns/treeview/examples/treeview-navigation/
|
||||||
|
//
|
||||||
|
|
||||||
|
const expandCollapse = (element) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopImmediatePropagation();
|
||||||
|
event.stopPropagation();
|
||||||
|
element.click();
|
||||||
|
};
|
||||||
|
|
||||||
|
if (event.target.classList.contains('nav-list-expander')) {
|
||||||
|
// If the event target is the expand/collapse arrow then toggle the state of the sub tree.
|
||||||
|
expandCollapse(event.target);
|
||||||
|
} else if (event.target.tagName === 'A') {
|
||||||
|
// If the event target is a link then follow the link.
|
||||||
|
event.target.click();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleArrowKey = () => {
|
||||||
|
|
||||||
|
//
|
||||||
|
// Arrow key navigation implementation.
|
||||||
|
// For reference: https://www.w3.org/WAI/ARIA/apg/patterns/treeview/examples/treeview-navigation/
|
||||||
|
//
|
||||||
|
|
||||||
|
const currentlyFocusedNavItem = navParent.querySelector('a:focus');
|
||||||
|
if (!currentlyFocusedNavItem) {
|
||||||
|
// If no item is focused then do nothing.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Preventing the default action prevents jankiness in the default scrolling
|
||||||
|
// of the navigation panel, and is left to the browser to handle it when
|
||||||
|
// .focus() is invoked instead.
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
// Get all of the navigation items that are visible, and that are NOT the expand/collapse arrow.
|
||||||
|
const allNavItems = Array.from(
|
||||||
|
navParent.querySelectorAll('a:not(.nav-list-expander)')
|
||||||
|
).filter(element => element.getBoundingClientRect().height > 0);
|
||||||
|
|
||||||
|
const currentlyFocusedNavItemIndex = allNavItems.indexOf(currentlyFocusedNavItem);
|
||||||
|
|
||||||
|
const toggleExpandCollapseState = () => {
|
||||||
|
const parentLi = currentlyFocusedNavItem.parentElement;
|
||||||
|
if (parentLi) {
|
||||||
|
const expander = Array.from(parentLi.children).find(element => element.classList.contains('nav-list-expander'));
|
||||||
|
if (expander) {
|
||||||
|
expander.click();
|
||||||
|
Array.from(parentLi.children).forEach((element) => {
|
||||||
|
const ariaExpanded = element.getAttribute('aria-expanded');
|
||||||
|
if (ariaExpanded === 'true') {
|
||||||
|
element.setAttribute('aria-expanded', 'false');
|
||||||
|
} else {
|
||||||
|
element.setAttribute('aria-expanded', 'true');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (event.key === 'ArrowUp') {
|
||||||
|
if (currentlyFocusedNavItemIndex > 0) {
|
||||||
|
allNavItems[currentlyFocusedNavItemIndex - 1].focus();
|
||||||
|
}
|
||||||
|
} else if (event.key === 'ArrowDown') {
|
||||||
|
if (currentlyFocusedNavItemIndex < allNavItems.length - 1) {
|
||||||
|
allNavItems[currentlyFocusedNavItemIndex + 1].focus();
|
||||||
|
}
|
||||||
|
} else if (event.key === 'ArrowLeft') {
|
||||||
|
if (currentlyFocusedNavItem.getAttribute('aria-expanded') === 'true') {
|
||||||
|
toggleExpandCollapseState();
|
||||||
|
} else {
|
||||||
|
const parentLi = currentlyFocusedNavItem.parentElement;
|
||||||
|
if (parentLi) {
|
||||||
|
if (currentlyFocusedNavItemIndex > 0) {
|
||||||
|
// The parent of the target <a> is a <li> which is a child of a <ul> which is a child of a <li>.
|
||||||
|
// This <li> should have a <a> that is the parent to focus unless the current is the top.
|
||||||
|
const listWrapperLi = currentlyFocusedNavItem?.parentElement?.parentElement?.parentElement;
|
||||||
|
if (listWrapperLi) {
|
||||||
|
const parentListAnchor = listWrapperLi.querySelectorAll('a:not(.nav-list-expander)')[0];
|
||||||
|
if (parentListAnchor) {
|
||||||
|
parentListAnchor.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (event.key === 'ArrowRight') {
|
||||||
|
const ariaExpanded = currentlyFocusedNavItem.getAttribute('aria-expanded');
|
||||||
|
if (ariaExpanded === 'false' || ariaExpanded === null) {
|
||||||
|
toggleExpandCollapseState();
|
||||||
|
} else {
|
||||||
|
const parentLi = currentlyFocusedNavItem.parentElement;
|
||||||
|
if (parentLi) {
|
||||||
|
const childList = Array.from(parentLi.children).find(element => element.tagName === 'UL');
|
||||||
|
if (childList) {
|
||||||
|
const childListAnchor = childList.querySelectorAll('a:not(.nav-list-expander)')[0];
|
||||||
|
if (childListAnchor) {
|
||||||
|
childListAnchor.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (event.key) {
|
||||||
|
case 'ArrowUp':
|
||||||
|
case 'ArrowDown':
|
||||||
|
case 'ArrowLeft':
|
||||||
|
case 'ArrowRight':
|
||||||
|
handleArrowKey();
|
||||||
|
break;
|
||||||
|
case ' ':
|
||||||
|
handleSpaceKey();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
|
@ -45,12 +45,6 @@
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
navToHighlightedResult();
|
navToHighlightedResult();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'Tab':
|
|
||||||
e.preventDefault();
|
|
||||||
highlightNextResult(!e.shiftKey);
|
|
||||||
break;
|
|
||||||
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue