Commit 4534acdc authored by Robert Knight's avatar Robert Knight

Handle focus requests in ThreadCard

When an annotation keyboard focus request has been created in the store,
handle it in the corresponding ThreadCard.
parent dae9dee0
import { Card } from '@hypothesis/frontend-shared';
import classnames from 'classnames';
import debounce from 'lodash.debounce';
import { useCallback, useMemo } from 'preact/hooks';
import { useCallback, useEffect, useMemo, useRef } from 'preact/hooks';
import { useSidebarStore } from '../store';
import { withServices } from '../service-context';
......@@ -63,6 +63,17 @@ function ThreadCard({ frameSync, thread }) {
// parent component but the `Thread` itself has not changed.
const threadContent = useMemo(() => <Thread thread={thread} />, [thread]);
// Handle requests to give this thread keyboard focus.
const focusRequest = store.annotationFocusRequest();
const cardRef = useRef(/** @type {HTMLElement|null} */ (null));
useEffect(() => {
if (focusRequest !== thread.id || !cardRef.current) {
return;
}
cardRef.current.focus();
store.clearAnnotationFocusRequest();
}, [focusRequest, store, thread.id]);
return (
/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */
<Card
......@@ -70,6 +81,8 @@ function ThreadCard({ frameSync, thread }) {
'is-hovered': isHovered,
})}
data-testid="thread-card"
elementRef={cardRef}
tabIndex={0}
onClick={e => {
// Prevent click events intended for another action from
// triggering a page scroll.
......
......@@ -6,6 +6,7 @@ import { checkAccessibility } from '../../../test-util/accessibility';
import { mockImportedComponents } from '../../../test-util/mock-imported-components';
describe('ThreadCard', () => {
let container;
let fakeDebounce;
let fakeFrameSync;
let fakeStore;
......@@ -15,17 +16,23 @@ describe('ThreadCard', () => {
function createComponent(props) {
return mount(
<ThreadCard frameSync={fakeFrameSync} thread={fakeThread} {...props} />
<ThreadCard frameSync={fakeFrameSync} thread={fakeThread} {...props} />,
{ attachTo: container }
);
}
beforeEach(() => {
container = document.createElement('div');
document.body.append(container);
fakeDebounce = sinon.stub().returnsArg(0);
fakeFrameSync = {
hoverAnnotations: sinon.stub(),
scrollToAnnotation: sinon.stub(),
};
fakeStore = {
annotationFocusRequest: sinon.stub().returns(null),
clearAnnotationFocusRequest: sinon.stub(),
isAnnotationHovered: sinon.stub().returns(false),
route: sinon.stub(),
};
......@@ -44,6 +51,7 @@ describe('ThreadCard', () => {
afterEach(() => {
$imports.$restore();
container.remove();
});
it('renders a `Thread` for the passed `thread`', () => {
......@@ -102,6 +110,31 @@ describe('ThreadCard', () => {
});
});
describe('keyboard focus request handling', () => {
[null, 'other-annotation'].forEach(focusRequest => {
it('does not focus thread if there is no matching focus request', () => {
fakeStore.annotationFocusRequest.returns(focusRequest);
createComponent();
const threadCard = container.querySelector(threadCardSelector);
assert.notEqual(document.activeElement, threadCard);
assert.notCalled(fakeStore.clearAnnotationFocusRequest);
});
});
it('gives focus to the thread if there is a matching focus request', () => {
fakeStore.annotationFocusRequest.returns('t1');
createComponent();
const threadCard = container.querySelector(threadCardSelector);
assert.equal(document.activeElement, threadCard);
assert.called(fakeStore.clearAnnotationFocusRequest);
});
});
it(
'should pass a11y checks',
checkAccessibility({
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment