Skip to content

Commit 1f6ced3

Browse files
committed
Make annotation cards focusable
1 parent 808c294 commit 1f6ced3

File tree

3 files changed

+64
-4
lines changed

3 files changed

+64
-4
lines changed

src/sidebar/components/ThreadCard.tsx

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,9 @@ function ThreadCard({ frameSync, thread }: ThreadCardProps) {
8181
)}
8282
data-testid="thread-card"
8383
elementRef={cardRef}
84-
tabIndex={-1}
84+
tabIndex={0}
85+
role="article"
86+
aria-label="Press Enter to scroll annotation into view"
8587
onClick={e => {
8688
// Prevent click events intended for another action from
8789
// triggering a page scroll.
@@ -91,6 +93,20 @@ function ThreadCard({ frameSync, thread }: ThreadCardProps) {
9193
}}
9294
onMouseEnter={() => setThreadHovered(thread.annotation ?? null)}
9395
onMouseLeave={() => setThreadHovered(null)}
96+
onKeyDown={e => {
97+
// Simulate default button behavior, where `Enter` and `Space` trigger
98+
// click action
99+
if (
100+
// Trigger event only if the target is the card itself, so that we do
101+
// not scroll to the annotation while editing it, or if the key is
102+
// pressed to interact with a child button or link.
103+
e.target === cardRef.current &&
104+
['Enter', ' '].includes(e.key) &&
105+
thread.annotation
106+
) {
107+
scrollToAnnotation(thread.annotation);
108+
}
109+
}}
94110
key={thread.id}
95111
>
96112
<CardContent>{threadContent}</CardContent>

src/sidebar/components/ThreadList.tsx

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,16 @@
1-
import { ListenerCollection } from '@hypothesis/frontend-shared';
1+
import {
2+
ListenerCollection,
3+
useArrowKeyNavigation,
4+
} from '@hypothesis/frontend-shared';
25
import classnames from 'classnames';
36
import debounce from 'lodash.debounce';
4-
import { useEffect, useLayoutEffect, useMemo, useState } from 'preact/hooks';
7+
import {
8+
useEffect,
9+
useLayoutEffect,
10+
useMemo,
11+
useRef,
12+
useState,
13+
} from 'preact/hooks';
514

615
import type { Annotation, EPUBContentSelector } from '../../types/api';
716
import type { Thread } from '../helpers/build-thread';
@@ -300,11 +309,18 @@ export default function ThreadList({ threads }: ThreadListProps) {
300309
});
301310
}, [visibleThreads]);
302311

312+
const listRef = useRef<HTMLDivElement | null>(null);
313+
useArrowKeyNavigation(listRef, {
314+
selector: 'div[role="article"]',
315+
horizontal: false,
316+
loop: false,
317+
});
318+
303319
return (
304320
// We use role="list"/role="listitem" rather than unstyled ul/li because
305321
// some screen readers do not treat them as lists if they don't explicitly
306322
// have bullets
307-
<div role="list">
323+
<div role="list" ref={listRef}>
308324
<div style={{ height: offscreenUpperHeight }} />
309325
{visibleThreads.map((child, index) => (
310326
<div

src/sidebar/components/test/ThreadCard-test.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,34 @@ describe('ThreadCard', () => {
169169
});
170170
});
171171

172+
describe('key down', () => {
173+
[
174+
{ key: 'Enter', shouldScroll: true },
175+
{ key: ' ', shouldScroll: true },
176+
{ key: 'ArrowUp', shouldScroll: false },
177+
{ key: 'Escape', shouldScroll: false },
178+
].forEach(({ key, shouldScroll }) => {
179+
it('scrolls to the annotation when Enter or Space are pressed on the `ThreadCard`', () => {
180+
const wrapper = createComponent();
181+
182+
wrapper.find(threadCardSelector).simulate('keydown', { key });
183+
184+
assert.equal(fakeFrameSync.scrollToAnnotation.called, shouldScroll);
185+
});
186+
});
187+
188+
it('does not scroll to annotation when key is pressed in `ThreadCard` targeting other element', () => {
189+
const wrapper = createComponent();
190+
191+
wrapper
192+
.find(threadCardSelector)
193+
.props()
194+
.onKeyDown({ key: 'Enter', target: document.createElement('a') });
195+
196+
assert.notCalled(fakeFrameSync.scrollToAnnotation);
197+
});
198+
});
199+
172200
it(
173201
'should pass a11y checks',
174202
checkAccessibility({

0 commit comments

Comments
 (0)