Commit 0b9a114f authored by Lyza Danger Gardner's avatar Lyza Danger Gardner Committed by Lyza Gardner

Adjust keyboard interaction with TagEditor

parent b32cbefb
......@@ -84,7 +84,6 @@ function TagEditor({ onEditTags, tags: tagsService, tagList }) {
setSuggestions(removeDuplicates(suggestions, tagList));
setSuggestionsListOpen(suggestions.length > 0);
}
setActiveItem(-1);
};
......@@ -194,44 +193,63 @@ function TagEditor({ onEditTags, tags: tagsService, tagList }) {
};
/**
* Keydown handler for keyboard navigation of the suggestions list
* and when the user presses "Enter" or ","" to add a new typed item not
* found in the suggestions list
* Keydown handler for keyboard navigation of the tag editor field and the
* suggested-tags list.
*
* @param {KeyboardEvent} e
*/
const handleKeyDown = e => {
switch (normalizeKeyName(e.key)) {
case 'ArrowUp':
// Select the previous item in the suggestion list
changeSelectedItem(-1);
e.preventDefault();
break;
case 'ArrowDown':
// Select the next item in the suggestion list
changeSelectedItem(1);
e.preventDefault();
break;
case 'Escape':
// Clear any entered text, but retain focus
inputEl.current.value = '';
e.preventDefault();
break;
case 'Enter':
case ',':
// Commit a tag
if (activeItem === -1) {
// nothing selected, just add the typed text
addTag(/** @type {HTMLInputElement} */ (inputEl.current).value);
} else {
// Add the selected tag
addTag(suggestions[activeItem]);
}
e.preventDefault();
break;
case 'Tab':
// Commit a tag, or tab out of the field if it is empty (default browser
// behavior)
if (inputEl.current.value.trim() === '') {
// If the tag field is empty, allow `Tab` to have its default
// behavior: continue to the next element in tab order
break;
}
if (activeItem !== -1) {
// If there is a selected item, then allow `Tab` to behave exactly
// like `Enter` or `,`.
// If there is a selected item in the suggested tag list,
// commit that tag (just like `Enter` and `,` in this case)
addTag(suggestions[activeItem]);
e.preventDefault();
} else if (suggestionsListOpen) {
// If there is no selected item, then allow `Tab` to add the first
// item in the list if the list is open.
} else if (suggestions.length === 1) {
// If there is exactly one suggested tag match, commit that tag
// This emulates a "tab-complete" behavior
addTag(suggestions[0]);
e.preventDefault();
} else {
// Commit the tag as typed in the field
addTag(/** @type {HTMLInputElement} */ (inputEl.current).value);
}
// Retain focus
e.preventDefault();
break;
}
};
......
......@@ -207,8 +207,9 @@ describe('TagEditor', function () {
*/
const assertAddTagsSuccess = (wrapper, tagList) => {
// saves the suggested tags to the service
assert.isTrue(
fakeTagsService.store.calledWith(tagList.map(tag => ({ text: tag })))
assert.calledWith(
fakeTagsService.store,
tagList.map(tag => ({ text: tag }))
);
// called the onEditTags callback prop
assert.isTrue(fakeOnEditTags.calledWith({ tags: tagList }));
......@@ -242,10 +243,10 @@ describe('TagEditor', function () {
].forEach(keyAction => {
it(`adds a tag from the <input> field when typing "${keyAction[1]}"`, () => {
const wrapper = createComponent();
wrapper.find('input').instance().value = 'tag3';
wrapper.find('input').instance().value = 'umbrella';
typeInput(wrapper); // opens suggestion list
keyAction[0](wrapper);
assertAddTagsSuccess(wrapper, ['tag1', 'tag2', 'tag3']);
assertAddTagsSuccess(wrapper, ['tag1', 'tag2', 'umbrella']);
// ensure focus is still on the input field
assert.equal(document.activeElement.nodeName, 'INPUT');
});
......@@ -268,49 +269,90 @@ describe('TagEditor', function () {
assert.equal(document.activeElement.nodeName, 'INPUT');
});
});
it('should not add a tag if the <input> is empty', () => {
const wrapper = createComponent();
wrapper.find('input').instance().value = '';
selectOptionViaEnter(wrapper);
assertAddTagsFail();
});
it('should not add a tag if the input is empty', () => {
const wrapper = createComponent();
wrapper.find('input').instance().value = '';
selectOptionViaEnter(wrapper);
assertAddTagsFail();
context('When using the "Escape" key', () => {
it('should clear tag text in <input> but retain focus', () => {
const wrapper = createComponent();
// Add and commit a tag
wrapper.find('input').instance().value = 'thankyou';
typeInput(wrapper);
wrapper.find('input').simulate('keydown', { key: 'Tab' });
// Type more text
wrapper.find('input').instance().value = 'food';
typeInput(wrapper);
// // Now press escape
wrapper.find('input').simulate('keydown', { key: 'Escape' });
assert.equal(wrapper.find('input').instance().value, '');
assert.equal(document.activeElement.nodeName, 'INPUT');
});
});
it('should not add a tag if the <input> value is only white space', () => {
const wrapper = createComponent();
wrapper.find('input').instance().value = ' ';
selectOptionViaEnter(wrapper);
assertAddTagsFail();
});
context('When using the "Enter" key', () => {
it('should not add a tag if the <input> is empty', () => {
const wrapper = createComponent();
wrapper.find('input').instance().value = '';
selectOptionViaEnter(wrapper);
assertAddTagsFail();
});
it('should not add a tag if its a duplicate of one already in the list', () => {
const wrapper = createComponent();
wrapper.find('input').instance().value = 'tag1';
selectOptionViaEnter(wrapper);
assertAddTagsFail();
});
it('should not add a tag if the <input> value is only white space', () => {
const wrapper = createComponent();
wrapper.find('input').instance().value = ' ';
selectOptionViaEnter(wrapper);
assertAddTagsFail();
});
it('should not add a tag when pressing "Tab" and there are no suggestions', () => {
const wrapper = createComponent();
fakeTagsService.filter.returns([]);
wrapper.find('input').instance().value = 'tag33';
typeInput(wrapper);
selectOptionViaTab(wrapper);
assertAddTagsFail();
it('should not add a tag if its a duplicate of one already in the list', () => {
const wrapper = createComponent();
wrapper.find('input').instance().value = 'tag1';
selectOptionViaEnter(wrapper);
assertAddTagsFail();
});
});
it('should not a tag when pressing "Tab" and no suggestions are found', () => {
const wrapper = createComponent();
wrapper.find('input').instance().value = 'tag3';
// note: typeInput() opens the suggestions list
selectOptionViaTab(wrapper);
assertAddTagsFail();
context('Using the "Tab" key', () => {
it('should add the tag as typed when there are no suggestions', () => {
const wrapper = createComponent();
fakeTagsService.filter.returns([]);
wrapper.find('input').instance().value = 'tag33';
typeInput(wrapper);
selectOptionViaTab(wrapper);
assertAddTagsSuccess(wrapper, ['tag1', 'tag2', 'tag33']);
// ensure focus is still on the input field
assert.equal(document.activeElement.nodeName, 'INPUT');
});
it('should add the tag as typed when there are multiple suggestions', () => {
const wrapper = createComponent();
fakeTagsService.filter.returns([]);
wrapper.find('input').instance().value = 't';
typeInput(wrapper);
selectOptionViaTab(wrapper);
assertAddTagsSuccess(wrapper, ['tag1', 'tag2', 't']);
// ensure focus is still on the input field
assert.equal(document.activeElement.nodeName, 'INPUT');
});
it('should add the suggested tag when there is exactly one suggestion', () => {
const wrapper = createComponent();
fakeTagsService.filter.returns(['tag3']);
wrapper.find('input').instance().value = 'tag';
typeInput(wrapper);
// suggestions: [tag3]
selectOptionViaTab(wrapper);
assertAddTagsSuccess(wrapper, ['tag1', 'tag2', 'tag3']);
// ensure focus is still on the input field
assert.equal(document.activeElement.nodeName, 'INPUT');
});
it('should allow navigation out of field when there is no <input> value', () => {
const wrapper = createComponent();
wrapper.find('input').instance().value = '';
typeInput(wrapper);
selectOptionViaTab(wrapper);
// Focus has moved
assert.equal(document.activeElement.nodeName, 'BODY');
});
});
});
......
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