Unverified Commit db161766 authored by Kyle Keating's avatar Kyle Keating Committed by GitHub

Merge pull request #1740 from hypothesis/tag-editor-a11y-tests

Improve a11y testing for AutocompleteList and TagEditor
parents 1ef0b98d 4c8c6867
...@@ -53,19 +53,25 @@ export default function AutocompleteList({ ...@@ -53,19 +53,25 @@ export default function AutocompleteList({
}, [activeItem, itemPrefixId, list, listFormatter, onSelectItem]); }, [activeItem, itemPrefixId, list, listFormatter, onSelectItem]);
const props = id ? { id } : {}; // only add the id if its passed const props = id ? { id } : {}; // only add the id if its passed
const isHidden = list.length === 0 || !open;
return ( return (
<div className="autocomplete-list"> <div
{list.length > 0 && open && ( className={classnames(
<ul {
className="autocomplete-list__items" 'is-hidden': isHidden,
tabIndex="-1" },
aria-label="Suggestions" 'autocomplete-list'
role="listbox"
{...props}
>
{items}
</ul>
)} )}
>
<ul
className="autocomplete-list__items"
tabIndex="-1"
aria-label="Suggestions"
role="listbox"
{...props}
>
{items}
</ul>
</div> </div>
); );
} }
......
...@@ -109,6 +109,7 @@ function TagEditor({ onEditTags, tags: tagsService, tagList }) { ...@@ -109,6 +109,7 @@ function TagEditor({ onEditTags, tags: tagsService, tagList }) {
} }
updateTags([...tagList, value]); updateTags([...tagList, value]);
setSuggestionsListOpen(false); setSuggestionsListOpen(false);
setActiveItem(-1);
inputEl.current.value = ''; inputEl.current.value = '';
inputEl.current.focus(); inputEl.current.focus();
...@@ -248,6 +249,7 @@ function TagEditor({ onEditTags, tags: tagsService, tagList }) { ...@@ -248,6 +249,7 @@ function TagEditor({ onEditTags, tags: tagsService, tagList }) {
className="tag-editor__input" className="tag-editor__input"
type="text" type="text"
autoComplete="off" autoComplete="off"
aria-label="Add tag field"
aria-autocomplete="list" aria-autocomplete="list"
aria-activedescendant={activeDescendant} aria-activedescendant={activeDescendant}
aria-controls={`${tagEditorId}-autocomplete-list`} aria-controls={`${tagEditorId}-autocomplete-list`}
......
...@@ -4,6 +4,7 @@ import { createElement } from 'preact'; ...@@ -4,6 +4,7 @@ import { createElement } from 'preact';
import AutocompleteList from '../autocomplete-list'; import AutocompleteList from '../autocomplete-list';
import { $imports } from '../autocomplete-list'; import { $imports } from '../autocomplete-list';
import { checkAccessibility } from '../../../test-util/accessibility';
import mockImportedComponents from '../../../test-util/mock-imported-components'; import mockImportedComponents from '../../../test-util/mock-imported-components';
describe('AutocompleteList', function() { describe('AutocompleteList', function() {
...@@ -33,12 +34,12 @@ describe('AutocompleteList', function() { ...@@ -33,12 +34,12 @@ describe('AutocompleteList', function() {
it('does not render the list when `open` is false', () => { it('does not render the list when `open` is false', () => {
const wrapper = createComponent(); const wrapper = createComponent();
assert.isFalse(wrapper.find('.autocomplete-list__items').exists()); assert.isTrue(wrapper.find('.autocomplete-list').hasClass('is-hidden'));
}); });
it('does not render the list when `list` is empty', () => { it('does not render the list when `list` is empty', () => {
const wrapper = createComponent({ open: true, list: [] }); const wrapper = createComponent({ open: true, list: [] });
assert.isFalse(wrapper.find('.autocomplete-list__items').exists()); assert.isTrue(wrapper.find('.autocomplete-list').hasClass('is-hidden'));
}); });
it('sets unique keys to the <li> items', () => { it('sets unique keys to the <li> items', () => {
...@@ -160,4 +161,22 @@ describe('AutocompleteList', function() { ...@@ -160,4 +161,22 @@ describe('AutocompleteList', function() {
.prop('aria-selected') .prop('aria-selected')
); );
}); });
it(
'should pass a11y checks',
checkAccessibility([
{
name: 'list open',
content: () => {
return createComponent({ open: true });
},
},
{
name: 'list open, first item selected',
content: () => {
return createComponent({ open: true, activeItem: 1 });
},
},
])
);
}); });
...@@ -2,6 +2,7 @@ import { mount } from 'enzyme'; ...@@ -2,6 +2,7 @@ import { mount } from 'enzyme';
import { createElement } from 'preact'; import { createElement } from 'preact';
import { act } from 'preact/test-utils'; import { act } from 'preact/test-utils';
import AutocompleteList from '../autocomplete-list';
import TagEditor from '../tag-editor'; import TagEditor from '../tag-editor';
import { $imports } from '../tag-editor'; import { $imports } from '../tag-editor';
...@@ -112,70 +113,6 @@ describe('TagEditor', function() { ...@@ -112,70 +113,6 @@ describe('TagEditor', function() {
assert.isTrue(fakeTagsService.filter.calledWith('tag3')); assert.isTrue(fakeTagsService.filter.calledWith('tag3'));
}); });
describe('accessibility attributes and ids', () => {
it('creates multiple <TagEditor> components with unique autocomplete-list `id` props', () => {
const wrapper1 = createComponent();
const wrapper2 = createComponent();
assert.notEqual(
wrapper1.find('AutocompleteList').prop('id'),
wrapper2.find('AutocompleteList').prop('id')
);
});
it('sets the <AutocompleteList> `id` prop to the same value as the `aria-owns` attribute', () => {
const wrapper = createComponent();
wrapper.find('AutocompleteList');
assert.equal(
wrapper.find('.tag-editor__combobox-wrapper').prop('aria-owns'),
wrapper.find('AutocompleteList').prop('id')
);
});
it('sets `aria-expanded` value to match open state', () => {
const wrapper = createComponent();
wrapper.find('input').instance().value = 'non-empty'; // to open list
typeInput(wrapper);
assert.equal(
wrapper.find('.tag-editor__combobox-wrapper').prop('aria-expanded'),
'true'
);
selectOption(wrapper, 'tag4');
wrapper.update();
assert.equal(
wrapper.find('.tag-editor__combobox-wrapper').prop('aria-expanded'),
'false'
);
});
it('sets the <AutocompleteList> `activeItem` prop to match the selected item index', () => {
function checkAttributes(wrapper) {
const activeDescendant = wrapper
.find('input')
.prop('aria-activedescendant');
const itemPrefixId = wrapper
.find('AutocompleteList')
.prop('itemPrefixId');
const activeDescendantIndex = activeDescendant.split(itemPrefixId);
assert.equal(
activeDescendantIndex[1],
wrapper.find('AutocompleteList').prop('activeItem')
);
}
const wrapper = createComponent();
wrapper.find('input').instance().value = 'non-empty';
typeInput(wrapper);
// initial aria-activedescendant value is "" when index is -1
assert.equal(wrapper.find('input').prop('aria-activedescendant'), '');
// 2 suggestions: ['tag3', 'tag4'];
navigateDown(wrapper); // press down once
checkAttributes(wrapper);
navigateDown(wrapper); // press down again once
checkAttributes(wrapper);
});
});
describe('suggestions open / close', () => { describe('suggestions open / close', () => {
it('closes the suggestions when selecting a tag from autocomplete-list', () => { it('closes the suggestions when selecting a tag from autocomplete-list', () => {
const wrapper = createComponent(); const wrapper = createComponent();
...@@ -271,6 +208,8 @@ describe('TagEditor', function() { ...@@ -271,6 +208,8 @@ describe('TagEditor', function() {
assert.isTrue(fakeOnEditTags.calledWith({ tags: tagList })); assert.isTrue(fakeOnEditTags.calledWith({ tags: tagList }));
// hides the suggestions // hides the suggestions
assert.equal(wrapper.find('AutocompleteList').prop('open'), false); assert.equal(wrapper.find('AutocompleteList').prop('open'), false);
// removes the selected index
assert.equal(wrapper.find('AutocompleteList').prop('activeItem'), -1);
// assert the input value is cleared out // assert the input value is cleared out
assert.equal(wrapper.find('input').instance().value, ''); assert.equal(wrapper.find('input').instance().value, '');
// input element should have focus // input element should have focus
...@@ -408,11 +347,107 @@ describe('TagEditor', function() { ...@@ -408,11 +347,107 @@ describe('TagEditor', function() {
}); });
}); });
// FIXME-A11Y describe('accessibility attributes and ids', () => {
it.skip( it('creates multiple <TagEditor> components with unique autocomplete-list `id` props', () => {
'should pass a11y checks', const wrapper1 = createComponent();
checkAccessibility({ const wrapper2 = createComponent();
content: () => createComponent(), assert.notEqual(
}) wrapper1.find('AutocompleteList').prop('id'),
); wrapper2.find('AutocompleteList').prop('id')
);
});
it('sets the <AutocompleteList> `id` prop to the same value as the `aria-owns` attribute', () => {
const wrapper = createComponent();
wrapper.find('AutocompleteList');
assert.equal(
wrapper.find('.tag-editor__combobox-wrapper').prop('aria-owns'),
wrapper.find('AutocompleteList').prop('id')
);
});
it('sets `aria-expanded` value to match open state', () => {
const wrapper = createComponent();
wrapper.find('input').instance().value = 'non-empty'; // to open list
typeInput(wrapper);
assert.equal(
wrapper.find('.tag-editor__combobox-wrapper').prop('aria-expanded'),
'true'
);
selectOption(wrapper, 'tag4');
wrapper.update();
assert.equal(
wrapper.find('.tag-editor__combobox-wrapper').prop('aria-expanded'),
'false'
);
});
it('sets the <AutocompleteList> `activeItem` prop to match the selected item index', () => {
function checkAttributes(wrapper) {
const activeDescendant = wrapper
.find('input')
.prop('aria-activedescendant');
const itemPrefixId = wrapper
.find('AutocompleteList')
.prop('itemPrefixId');
const activeDescendantIndex = activeDescendant.split(itemPrefixId);
assert.equal(
activeDescendantIndex[1],
wrapper.find('AutocompleteList').prop('activeItem')
);
}
const wrapper = createComponent();
wrapper.find('input').instance().value = 'non-empty';
typeInput(wrapper);
// initial aria-activedescendant value is "" when index is -1
assert.equal(wrapper.find('input').prop('aria-activedescendant'), '');
// 2 suggestions: ['tag3', 'tag4'];
navigateDown(wrapper); // press down once
checkAttributes(wrapper);
navigateDown(wrapper); // press down again once
checkAttributes(wrapper);
});
});
describe('accessibility validation', () => {
beforeEach(function() {
// create a full dom tree for a11y testing
$imports.$mock({
'./autocomplete-list': AutocompleteList,
});
});
it(
'should pass a11y checks',
checkAccessibility([
{
name: 'suggestions open',
content: () => {
const wrapper = createComponent();
wrapper.find('input').instance().value = 'non-empty';
typeInput(wrapper);
return wrapper;
},
},
{
name: 'suggestions open, first item selected',
content: () => {
const wrapper = createComponent();
wrapper.find('input').instance().value = 'non-empty';
typeInput(wrapper);
navigateDown(wrapper);
return wrapper;
},
},
{
name: 'suggestions closed',
content: () => {
return createComponent();
},
},
])
);
});
}); });
...@@ -2,6 +2,9 @@ ...@@ -2,6 +2,9 @@
.autocomplete-list { .autocomplete-list {
position: relative; position: relative;
&.is-hidden {
display: none;
}
} }
.autocomplete-list__items { .autocomplete-list__items {
......
...@@ -20,7 +20,7 @@ $grey-4: #a6a6a6; ...@@ -20,7 +20,7 @@ $grey-4: #a6a6a6;
// minus blue tint. // minus blue tint.
$grey-semi: #9c9c9c; $grey-semi: #9c9c9c;
$grey-5: #767676; $grey-5: #737373;
// Interim color variable for migration purposes, as the step between `$grey-5` // Interim color variable for migration purposes, as the step between `$grey-5`
// and `$grey-6` is large. Represents `base-mid` in proposed future palette, // and `$grey-6` is large. Represents `base-mid` in proposed future palette,
......
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