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