Commit 1720fab6 authored by Lyza Danger Gardner's avatar Lyza Danger Gardner Committed by Lyza Gardner

Add `aria-label` attribute to Annotation `article` elements

parent 739efa8b
import { Actions, Spinner } from '@hypothesis/frontend-shared'; import { Actions, Spinner } from '@hypothesis/frontend-shared';
import classnames from 'classnames'; import classnames from 'classnames';
import { useMemo } from 'preact/hooks';
import { useSidebarStore } from '../../store'; import { useSidebarStore } from '../../store';
import { isOrphan, isSaved, quote } from '../../helpers/annotation-metadata'; import {
annotationRole,
isOrphan,
isSaved,
quote,
} from '../../helpers/annotation-metadata';
import { annotationDisplayName } from '../../helpers/annotation-user';
import { withServices } from '../../service-context'; import { withServices } from '../../service-context';
import AnnotationActionBar from './AnnotationActionBar'; import AnnotationActionBar from './AnnotationActionBar';
...@@ -84,8 +91,16 @@ function Annotation({ ...@@ -84,8 +91,16 @@ function Annotation({
} }
}; };
const authorName = useMemo(
() => annotationDisplayName(annotation, store),
[annotation, store]
);
return ( return (
<article className="space-y-4"> <article
className="space-y-4"
aria-label={`${annotationRole(annotation)} by ${authorName}`}
>
<AnnotationHeader <AnnotationHeader
annotation={annotation} annotation={annotation}
isEditing={isEditing} isEditing={isEditing}
......
...@@ -10,6 +10,7 @@ import Annotation, { $imports } from '../Annotation'; ...@@ -10,6 +10,7 @@ import Annotation, { $imports } from '../Annotation';
describe('Annotation', () => { describe('Annotation', () => {
// Dependency Mocks // Dependency Mocks
let fakeMetadata; let fakeMetadata;
let fakeAnnotationUser;
// Injected dependency mocks // Injected dependency mocks
let fakeAnnotationsService; let fakeAnnotationsService;
...@@ -43,7 +44,12 @@ describe('Annotation', () => { ...@@ -43,7 +44,12 @@ describe('Annotation', () => {
save: sinon.stub().resolves(), save: sinon.stub().resolves(),
}; };
fakeAnnotationUser = {
annotationDisplayName: sinon.stub().returns('Richard Lionheart'),
};
fakeMetadata = { fakeMetadata = {
annotationRole: sinon.stub().returns('Annotation'),
quote: sinon.stub(), quote: sinon.stub(),
}; };
...@@ -58,6 +64,7 @@ describe('Annotation', () => { ...@@ -58,6 +64,7 @@ describe('Annotation', () => {
$imports.$mock(mockImportedComponents()); $imports.$mock(mockImportedComponents());
$imports.$mock({ $imports.$mock({
'../../helpers/annotation-metadata': fakeMetadata, '../../helpers/annotation-metadata': fakeMetadata,
'../../helpers/annotation-user': fakeAnnotationUser,
'../../store': { useSidebarStore: () => fakeStore }, '../../store': { useSidebarStore: () => fakeStore },
}); });
}); });
...@@ -66,6 +73,17 @@ describe('Annotation', () => { ...@@ -66,6 +73,17 @@ describe('Annotation', () => {
$imports.$restore(); $imports.$restore();
}); });
describe('annotation accessibility (ARIA) attributes', () => {
it('should add an `aria-label` composed of annotation type and author name', () => {
const wrapper = createComponent();
assert.equal(
wrapper.find('article').props()['aria-label'],
'Annotation by Richard Lionheart'
);
});
});
describe('annotation quote', () => { describe('annotation quote', () => {
it('renders quote if annotation has a quote', () => { it('renders quote if annotation has a quote', () => {
fakeMetadata.quote.returns('quote'); fakeMetadata.quote.returns('quote');
......
...@@ -276,6 +276,22 @@ export function isAnnotation(annotation) { ...@@ -276,6 +276,22 @@ export function isAnnotation(annotation) {
return !!(hasSelector(annotation) && !isOrphan(annotation)); return !!(hasSelector(annotation) && !isOrphan(annotation));
} }
/**
* Return a human-readable string describing the annotation's role.
*
* @param {Annotation} annotation
*/
export function annotationRole(annotation) {
if (isReply(annotation)) {
return 'Reply';
} else if (isHighlight(annotation)) {
return 'Highlight';
} else if (isPageNote(annotation)) {
return 'Page note';
}
return 'Annotation';
}
/** Return a numeric key that can be used to sort annotations by location. /** Return a numeric key that can be used to sort annotations by location.
* *
* @param {Annotation} annotation * @param {Annotation} annotation
......
...@@ -272,7 +272,7 @@ describe('sidebar/helpers/annotation-metadata', () => { ...@@ -272,7 +272,7 @@ describe('sidebar/helpers/annotation-metadata', () => {
}); });
}); });
describe('.isHighlight', () => { describe('isHighlight', () => {
[ [
{ {
annotation: fixtures.newEmptyAnnotation(), annotation: fixtures.newEmptyAnnotation(),
...@@ -373,6 +373,47 @@ describe('sidebar/helpers/annotation-metadata', () => { ...@@ -373,6 +373,47 @@ describe('sidebar/helpers/annotation-metadata', () => {
}); });
}); });
describe('annotationRole', () => {
it('correctly identifies the role of an annotation', () => {
// An annotation needs a `selector` or else it will be identified as a
// 'Page note'
const annotationAnnotation = {
...fixtures.newAnnotation(),
target: [{ source: 'source', selector: [] }],
};
const highlightAnnotation = fixtures.oldHighlight();
const pageNoteAnnotation = fixtures.newPageNote();
// If an annotation is a reply of any sort, that will supersede.
// e.g. the label for a page note that is also a reply is "Reply"
// In practice, highlights are never replies.
const replyAnnotations = [
{ ...annotationAnnotation, references: ['parent_annotation_id'] },
{ ...highlightAnnotation, references: ['parent_annotation_id'] },
{ ...pageNoteAnnotation, references: ['parent_annotation_id'] },
fixtures.oldReply(),
];
assert.equal(
annotationMetadata.annotationRole(annotationAnnotation),
'Annotation'
);
assert.equal(
annotationMetadata.annotationRole(highlightAnnotation),
'Highlight'
);
assert.equal(
annotationMetadata.annotationRole(pageNoteAnnotation),
'Page note'
);
replyAnnotations.forEach(reply => {
assert.equal(annotationMetadata.annotationRole(reply), 'Reply');
});
});
});
describe('isPublic', () => { describe('isPublic', () => {
it('returns true if an annotation is shared within a group', () => { it('returns true if an annotation is shared within a group', () => {
assert.isTrue(annotationMetadata.isPublic(fixtures.publicAnnotation())); assert.isTrue(annotationMetadata.isPublic(fixtures.publicAnnotation()));
......
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