Reputation: 345
To make an application compliant to the W3C feed pattern, I must create keyboard commands that help screen reader users browse content loaded via infinite scrolling. See working example here.
On the example page, focus on one of the items in the list, then press PAGE_DOWN/PAGE_UP. See? That lets you navigate the list items, while skipping each item's contents.
If you focus on a button within one of the items and try to navigate from there, it will still navigate correctly from article to article. That's how I want my application to behave, but it doesn't.
My code is essentially the same as in the example. Multiple <article>
elements inside a <section role="feed">
. Using jQuery, I attach the 'keydown' event to that <section>
, referred to as #product-grid
.
$('#product-grid').keydown(function(event) {
// Detect which key was pressed and execute appropriate action
});
The HTML structure:
<section id="product-grid" role="feed">
<article tabindex="-1">
<a href="#"> having focus on this element prevents the 'keydown' event from firing
<img src="..."/>
<p>some text</p>
</a>
<div>
if you click on this non-interactive section instead, the event fires correctly
</div>
</article>
(...) many other <article> elements
</section>
Inside my articles, there are anchors. If you focus on them and press a key, the 'keydown' event will not fire. If you focus anywhere else on the article, it does.
It is my understanding that when a descendant item has focus, the parent (#product-grid
in this case) is also in focus. Am I correct?
Things I've tried:
#product-grid
, including the anchor element.<article>
directly. Nothing happens when I put focus on it and press a key. Based on what I found by searching here on SO, this is supposed to work.This is a fiddle in which you can reproduce the problem: https://jsfiddle.net/fgzom4kw/
To reproduce the problem:
Compare this behavior with the behavior of the W3C example.
Problem solved. There are two solutions:
My way: https://stackoverflow.com/a/59449938/9811172
<matrixRef>
or the highway! </matrixRef>
Twisty's way: https://stackoverflow.com/a/59448891/9811172
Check the comments for any caveats with them.
Upvotes: 1
Views: 981
Reputation: 345
I figured it out. And so did Twisty.
The answer is in the JavaScript code of the W3C example, right here: https://www.w3.org/TR/wai-aria-practices-1.1/examples/feed/js/feed.js
var focusedArticle =
aria.Utils.matches(event.target, '[role="article"]') ?
event.target :
aria.Utils.getAncestorBySelector(event.target, '[role="article"]');
It tries to find out what element has focus at the time of the keypress.
event.target
is an <article>
or (as in the example) <div role="article">
, it will use that element.event.target
is not an article, it will attempt to retrieve the "closest" article it can find.This means that, if an interactive widget within an article has focus at the time we press one of our keyboard commands, the article that contains that widget will be used instead. The article elements are special because they contain metadata (aria-posinset, aria-setsize) used in my event handler (and by screen readers). The trick is in that getAncestorBySelector
method.
This is how it works now:
$('#product-grid').keydown(function(event) {
var $focusedItem = $(event.target);
// if focused element is not <article>, find the closest <article>
if ($focusedItem.is(':not(article)')) {
$focusedItem = $focusedItem.closest('article');
}
var itemIndex = $focusedItem.attr('aria-posinset');
var itemCount = $focusedItem.attr('aria-setsize');
(...)
..and problem solved :D
Fiddle with my solution: https://jsfiddle.net/5sta3o82/
Upvotes: 1
Reputation: 30883
I was unable to replicate the issue as you stated it. That said, you need a more widespread selector. Basically, if the User has focus on any items that are descendants of the selector, you want to bind the keydown
callback. Consider the following.
$(function() {
$("#product-grid").children().keydown(function(e) {
var el = $(e.target);
var ch = e.which;
console.log("Event:", e.type, "HTML Target:", el.prop("nodeName"), "Char:", ch);
});
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<section id="product-grid" role="feed">
<article tabindex="-1">
<a href="#"> having focus on this element prevents the 'keydown' event from firing
<img src="..."/>
<p>some text</p>
</a>
<div>
if you click on this non-interactive section instead, the event fires correctly
</div>
</article>
<article tabindex="-1">
<a href="#"> having focus on this element prevents the 'keydown' event from firing
<img src="..."/>
<p>some text</p>
</a>
<div>
if you click on this non-interactive section instead, the event fires correctly
</div>
</article>
<article tabindex="-1">
<a href="#"> having focus on this element prevents the 'keydown' event from firing
<img src="..."/>
<p>some text</p>
</a>
<div>
if you click on this non-interactive section instead, the event fires correctly
</div>
</article>
<article tabindex="-1">
<a href="#"> having focus on this element prevents the 'keydown' event from firing
<img src="..."/>
<p>some text</p>
</a>
<div>
if you click on this non-interactive section instead, the event fires correctly
</div>
</article>
<article tabindex="-1">
<a href="#"> having focus on this element prevents the 'keydown' event from firing
<img src="..."/>
<p>some text</p>
</a>
<div>
if you click on this non-interactive section instead, the event fires correctly
</div>
</article>
<article tabindex="-1">
<a href="#"> having focus on this element prevents the 'keydown' event from firing
<img src="..."/>
<p>some text</p>
</a>
<div>
if you click on this non-interactive section instead, the event fires correctly
</div>
</article>
<article tabindex="-1">
<a href="#"> having focus on this element prevents the 'keydown' event from firing
<img src="..."/>
<p>some text</p>
</a>
<div>
if you click on this non-interactive section instead, the event fires correctly
</div>
</article>
<article tabindex="-1">
<a href="#"> having focus on this element prevents the 'keydown' event from firing
<img src="..."/>
<p>some text</p>
</a>
<div>
if you click on this non-interactive section instead, the event fires correctly
</div>
</article>
<article tabindex="-1">
<a href="#"> having focus on this element prevents the 'keydown' event from firing
<img src="..."/>
<p>some text</p>
</a>
<div>
if you click on this non-interactive section instead, the event fires correctly
</div>
</article>
<article tabindex="-1">
<a href="#"> having focus on this element prevents the 'keydown' event from firing
<img src="..."/>
<p>some text</p>
</a>
<div>
if you click on this non-interactive section instead, the event fires correctly
</div>
</article>
<article tabindex="-1">
<a href="#"> having focus on this element prevents the 'keydown' event from firing
<img src="..."/>
<p>some text</p>
</a>
<div>
if you click on this non-interactive section instead, the event fires correctly
</div>
</article>
</section>
Using $("#product-grid").children()
as the selector will grab all child elements. You can then bind the callback as needed.
Update
The root issue was the current focus when using a tabbable element. Take a look at new example: https://jsfiddle.net/Twisty/m1w2b7rv/39/
JavaScript
$(function() {
function setFocus(i) {
$("[aria-posinset='" + i + "']").focus();
}
function navFocus(c, i, a) {
switch (c) {
case 33: // PAGE_UP
if (i > 1) {
setFocus(--i);
}
break;
case 34: // PAGE_DOWN
if (i < a) {
setFocus(++i);
}
break;
case 35: // END
if (event.ctrlKey) {
if (i !== a) {
setFocus(a);
}
}
break;
case 36: // HOME
if (event.ctrlKey) {
if (i !== 1) {
setFocus(1);
}
}
break;
}
}
$("#product-grid").on("keydown", function(event) {
var el = $(event.target);
if (el.prop("nodeName") == "A") {
el = el.closest("article");
el.focus();
}
var itemIndex = el.attr('aria-posinset');
var itemCount = el.attr('aria-setsize');
navFocus(event.which, itemIndex, itemCount);
});
});
The keydown
event is bubbling up and is being triggered, yet the current article didn't have focus
, so there was not a good way to get the proper index of the article. So if the focus was on the link, we had to reset the focus back to the article
element first.
Upvotes: 2