Commit 6bd09e71 authored by Lyza Danger Gardner's avatar Lyza Danger Gardner Committed by Lyza Gardner

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
parent 478c173c
...@@ -244,15 +244,32 @@ function TagEditor({ ...@@ -244,15 +244,32 @@ function TagEditor({
* @param {string} item - Suggested tag * @param {string} item - Suggested tag
* @return {JSXElement} - Formatted tag for use in list * @return {JSXElement} - Formatted tag for use in list
*/ */
const formatSuggestItem = item => { const formatSuggestedItem = item => {
const curVal = pendingTag(); // filtering of tags is case-insensitive
const prefix = item.slice(0, item.indexOf(curVal)); const curVal = pendingTag().toLowerCase();
const suffix = item.slice(item.indexOf(curVal) + curVal.length); 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 <span>{item}</span>;
}
// 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 ( return (
<span> <span>
<strong>{prefix}</strong> <strong>{prefix}</strong>
{curVal} {matchString}
<strong>{suffix}</strong> <strong>{suffix}</strong>
</span> </span>
); );
...@@ -324,7 +341,7 @@ function TagEditor({ ...@@ -324,7 +341,7 @@ function TagEditor({
<AutocompleteList <AutocompleteList
id={`${tagEditorId}-autocomplete-list`} id={`${tagEditorId}-autocomplete-list`}
list={suggestions} list={suggestions}
listFormatter={formatSuggestItem} listFormatter={formatSuggestedItem}
open={suggestionsListOpen} open={suggestionsListOpen}
onSelectItem={handleSelect} onSelectItem={handleSelect}
itemPrefixId={`${tagEditorId}-autocomplete-list-item-`} itemPrefixId={`${tagEditorId}-autocomplete-list-item-`}
......
...@@ -116,6 +116,56 @@ describe('TagEditor', function () { ...@@ -116,6 +116,56 @@ describe('TagEditor', function () {
assert.equal(wrapper.find('AutocompleteList').prop('list')[1], 'tag4'); assert.equal(wrapper.find('AutocompleteList').prop('list')[1], 'tag4');
}); });
it('shows case-insensitive matches to suggested tags', () => {
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', () => { it('passes the text value to filter() after receiving input', () => {
const wrapper = createComponent(); const wrapper = createComponent();
wrapper.find('input').instance().value = 'tag3'; wrapper.find('input').instance().value = 'tag3';
......
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