From 6bd09e7155b7290cb0ad71f9eede73c6681a42dd Mon Sep 17 00:00:00 2001 From: Lyza Danger Gardner Date: Sat, 19 Sep 2020 11:50:03 -0400 Subject: [PATCH] Fix case-sensitivity issue with rendering suggested tags. The `TagEditor` component formerly did not take casing into account in its formatting function for suggested tags. The match should be case-insensitive in the formatter, as it is in the service that does the filtering of tags. This is fixed in two ways: 1. Make substring matching in the formatting function case-insensitive. Render the substring match according to the suggested tag's casing. Fixes this issue specifically. 2. Provide a fallback in the formatter for when the input text does not "seem" to match the suggested `item`. In these cases, just render the suggested tag as-is. This will prevent the formatter from spazzing out if its notion of matching differs from the tag-service's in any future case. Fixes #2547 --- src/sidebar/components/tag-editor.js | 29 ++++++++--- .../components/test/tag-editor-test.js | 50 +++++++++++++++++++ 2 files changed, 73 insertions(+), 6 deletions(-) diff --git a/src/sidebar/components/tag-editor.js b/src/sidebar/components/tag-editor.js index 0280cef66cc..b470ecf24c8 100644 --- a/src/sidebar/components/tag-editor.js +++ b/src/sidebar/components/tag-editor.js @@ -244,15 +244,32 @@ function TagEditor({ * @param {string} item - Suggested tag * @return {JSXElement} - Formatted tag for use in list */ - const formatSuggestItem = item => { - const curVal = pendingTag(); - const prefix = item.slice(0, item.indexOf(curVal)); - const suffix = item.slice(item.indexOf(curVal) + curVal.length); + const formatSuggestedItem = item => { + // filtering of tags is case-insensitive + const curVal = pendingTag().toLowerCase(); + const suggestedTag = item.toLowerCase(); + const matchIndex = suggestedTag.indexOf(curVal); + + // If the current input doesn't seem to match the suggested tag, + // just render the tag as-is. + if (matchIndex === -1) { + return {item}; + } + + // Break the suggested tag into three parts: + // 1. Substring of the suggested tag that occurs before the match + // with the current input + const prefix = item.slice(0, matchIndex); + // 2. Substring of the suggested tag that matches the input text. NB: + // This may be in a different case than the input text. + const matchString = item.slice(matchIndex, matchIndex + curVal.length); + // 3. Substring of the suggested tag that occurs after the matched input + const suffix = item.slice(matchIndex + curVal.length); return ( {prefix} - {curVal} + {matchString} {suffix} ); @@ -324,7 +341,7 @@ function TagEditor({ { + fakeTagsService.filter.returns(['fine AArdvark', 'AAArgh']); + const wrapper = createComponent(); + wrapper.find('input').instance().value = 'aa'; + typeInput(wrapper); + + const formattingFn = wrapper.find('AutocompleteList').prop('listFormatter'); + const tagList = wrapper.find('AutocompleteList').prop('list'); + + const firstSuggestedTag = mount(formattingFn(tagList[0])) + .find('span') + .text(); + const secondSuggestedTag = mount(formattingFn(tagList[1])) + .find('span') + .text(); + + // Even though the entered text was lower case ('aa'), the suggested tag + // should be rendered with its original casing (upper-case here) + assert.equal(firstSuggestedTag, 'AAArgh'); + assert.equal(secondSuggestedTag, 'fine AArdvark'); + }); + + it('shows suggested tags as-is if they do not seem to match the input', () => { + // This case addresses a situation in which a substring match isn't found + // for the current input text against a given suggested tag. This should not + // happen in practice—i.e. filtered tags should match the current input— + // but there is no contract that the tags service filtering uses the same + // "matching" as the component, so we should be able to handle cases where + // there doesn't "seem" to be a match by just rendering the suggested tag + // as-is. + fakeTagsService.filter.returns(['fine AArdvark', 'AAArgh']); + const wrapper = createComponent(); + wrapper.find('input').instance().value = 'bb'; + typeInput(wrapper); + + const formattingFn = wrapper.find('AutocompleteList').prop('listFormatter'); + const tagList = wrapper.find('AutocompleteList').prop('list'); + + const firstSuggestedTag = mount(formattingFn(tagList[0])) + .find('span') + .text(); + const secondSuggestedTag = mount(formattingFn(tagList[1])) + .find('span') + .text(); + + // Obviously, these don't have a `bb` substring; we'll just render them... + assert.equal(firstSuggestedTag, 'AAArgh'); + assert.equal(secondSuggestedTag, 'fine AArdvark'); + }); + it('passes the text value to filter() after receiving input', () => { const wrapper = createComponent(); wrapper.find('input').instance().value = 'tag3';