From 7d9b0ac76b7eaae83567ceae036fd4ceb1f29c38 Mon Sep 17 00:00:00 2001
From: Precious Onyenaucheya
<86783201+precious-onyenaucheya-ons@users.noreply.github.com>
Date: Thu, 7 Nov 2024 15:31:53 +0000
Subject: [PATCH] Update Fuse.js configuration to adjust search threshold
(#3288)
* increase search threshold
* update threshold
* add option to extend search
* update logic
* update autosggest
* update validation
* Update src/components/autosuggest/example-autosuggest-country.njk
Co-authored-by: rmccar <42928680+rmccar@users.noreply.github.com>
* Update src/components/autosuggest/autosuggest.ui.js
Co-authored-by: rmccar <42928680+rmccar@users.noreply.github.com>
* rename to resultsThreshold
* update
* update
* update test
* update fuse config
* update param type in md file
* update from tenary to if else statement block
* update fuse version
* fix issue with search
* fix failing tests
* remove log
---------
Co-authored-by: rmccar <42928680+rmccar@users.noreply.github.com>
---
package.json | 2 +-
src/components/autosuggest/_macro-options.md | 41 ++++++++--------
src/components/autosuggest/_macro.njk | 1 +
src/components/autosuggest/_macro.spec.js | 8 +++-
src/components/autosuggest/autosuggest.ui.js | 48 +++++++++++++++----
.../example-autosuggest-country.njk | 3 +-
src/components/autosuggest/fuse-config.js | 9 +++-
src/js/analytics.js | 2 +-
yarn.lock | 39 +++------------
9 files changed, 85 insertions(+), 68 deletions(-)
diff --git a/package.json b/package.json
index 3555c80139..1d843290b9 100644
--- a/package.json
+++ b/package.json
@@ -84,7 +84,7 @@
"express": "^4.17.1",
"front-matter": "^4.0.2",
"fs-extra": "^11.1.1",
- "fuse.js": "^3.6.1",
+ "fuse.js": "^7.0.0",
"glob": "^10.2.3",
"gulp": "^4.0.2",
"gulp-babel": "^8.0.0",
diff --git a/src/components/autosuggest/_macro-options.md b/src/components/autosuggest/_macro-options.md
index e169e62837..90f8b166ab 100644
--- a/src/components/autosuggest/_macro-options.md
+++ b/src/components/autosuggest/_macro-options.md
@@ -1,20 +1,21 @@
-| Name | Type | Required | Description |
-| ------------------- | ------------------------------------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
-| autosuggestData | string | false | URL of the JSON file with the autosuggest data that needs to be searched. Required if not using the address index api |
-| allowMultiple | boolean | false | Allows the component to accept multiple selections |
-| instructions | string | true | Instructions on how to use the autosuggest that will be read out by screen readers |
-| ariaYouHaveSelected | string | true | Aria message to tell the user that they have selected an answer |
-| ariaMinChars | string | true | Aria message to tell the user how many characters they need to enter before autosuggest will start |
-| minChars | integer | false | Minimum number of characters to run a query. Default is 3 |
-| ariaOneResult | string | true | Aria message to tell the user there is only one suggestion left |
-| ariaNResults | string | true | Aria message to tell the user how many suggestions are left |
-| ariaLimitedResults | string | true | Aria message to tell the user if the results have been limited and what they are limited to |
-| moreResults | string | true | Aria message to tell the user to continue to type to refine suggestions |
-| noResults | string | true | message to tell the user there are no results |
-| tooManyResults | string | false | message to tell the user there are too many results to display and the user should refine the search. This is only required when using the address index api |
-| typeMore | string | true | message to encourage the user to enter more characters to get suggestions |
-| resultsTitle | string | true | Title of results to be displayed on screen at the top of the results |
-| resultsTitleId | string | true | ID for the results title. The ID is used in the results `aria-labelledby` to provide context for the results |
-| input | `Input` [_(ref)_](/components/input) | true | Configuration object for the input |
-| language | string | false | The ISO 639-1 Code will override the default language in page. Please note that only 'en', 'cy' and 'ni' is currently supported |
-| id | string | false | The `id` of the input |
+| Name | Type | Required | Description |
+| ------------------- | ------------------------------------ | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| autosuggestData | string | false | URL of the JSON file with the autosuggest data that needs to be searched. Required if not using the address index api |
+| allowMultiple | boolean | false | Allows the component to accept multiple selections |
+| instructions | string | true | Instructions on how to use the autosuggest that will be read out by screen readers |
+| ariaYouHaveSelected | string | true | Aria message to tell the user that they have selected an answer |
+| ariaMinChars | string | true | Aria message to tell the user how many characters they need to enter before autosuggest will start |
+| minChars | integer | false | Minimum number of characters to run a query. Default is 3 |
+| ariaOneResult | string | true | Aria message to tell the user there is only one suggestion left |
+| ariaNResults | string | true | Aria message to tell the user how many suggestions are left |
+| ariaLimitedResults | string | true | Aria message to tell the user if the results have been limited and what they are limited to |
+| moreResults | string | true | Aria message to tell the user to continue to type to refine suggestions |
+| noResults | string | true | message to tell the user there are no results |
+| tooManyResults | string | false | message to tell the user there are too many results to display and the user should refine the search. This is only required when using the address index api |
+| typeMore | string | true | message to encourage the user to enter more characters to get suggestions |
+| resultsTitle | string | true | Title of results to be displayed on screen at the top of the results |
+| resultsTitleId | string | true | ID for the results title. The ID is used in the results `aria-labelledby` to provide context for the results |
+| input | `Input` [_(ref)_](/components/input) | true | Configuration object for the input |
+| language | string | false | The ISO 639-1 Code will override the default language in page. Please note that only 'en', 'cy' and 'ni' is currently supported |
+| resultsThreshold | float | false | Option to adjust the search threshold and fuzziness. Accepts a range from 0 to 1, where 0 provides the closest match and 1 allows for more distant matches. Defaults to 0.2. |
+| id | string | false | The `id` of the input |
diff --git a/src/components/autosuggest/_macro.njk b/src/components/autosuggest/_macro.njk
index b8c2f4c52f..a5d18997e6 100644
--- a/src/components/autosuggest/_macro.njk
+++ b/src/components/autosuggest/_macro.njk
@@ -15,6 +15,7 @@
data-results-title="{{ params.resultsTitle }}"
data-no-results="{{ params.noResults }}"
data-type-more="{{ params.typeMore }}"
+ {% if params.resultsThreshold %}data-result-threshold="{{ params.resultsThreshold }}"{% endif %}
{% if params.apiDomain %}data-api-domain="{{ params.apiDomain }}"{% endif %}
{% if params.apiDomainBearerToken %}data-authorization-token="{{ params.apiDomainBearerToken }}"{% endif %}
{% if params.apiManualQueryParams == true %}data-query-params=""{% endif %}
diff --git a/src/components/autosuggest/_macro.spec.js b/src/components/autosuggest/_macro.spec.js
index efac0908e7..ee9c897fdf 100644
--- a/src/components/autosuggest/_macro.spec.js
+++ b/src/components/autosuggest/_macro.spec.js
@@ -32,6 +32,11 @@ const EXAMPLE_AUTOSUGGEST = {
typeMore: 'Continue entering to get suggestions',
};
+const EXAMPLE_AUTOSUGGEST_WITH_RESULTS_THRESHOLD = {
+ ...EXAMPLE_AUTOSUGGEST,
+ resultsThreshold: 0.5,
+};
+
describe('macro: autosuggest', () => {
it('passes jest-axe checks', async () => {
const $ = cheerio.load(renderComponent('autosuggest', EXAMPLE_AUTOSUGGEST));
@@ -47,7 +52,7 @@ describe('macro: autosuggest', () => {
});
it('has the provided data attributes', () => {
- const $ = cheerio.load(renderComponent('autosuggest', EXAMPLE_AUTOSUGGEST));
+ const $ = cheerio.load(renderComponent('autosuggest', EXAMPLE_AUTOSUGGEST_WITH_RESULTS_THRESHOLD));
const $element = $('.ons-autosuggest');
expect($element.attr('data-allow-multiple')).toBeUndefined();
@@ -63,6 +68,7 @@ describe('macro: autosuggest', () => {
expect($element.attr('data-no-results')).toBe('No suggestions found. You can enter your own answer');
expect($element.attr('data-results-title')).toBe('Suggestions');
expect($element.attr('data-type-more')).toBe('Continue entering to get suggestions');
+ expect($element.attr('data-result-threshold')).toBe('0.5');
});
it('has the `data-allow-multiple` attribute when `allowMultiple` is `true`', () => {
diff --git a/src/components/autosuggest/autosuggest.ui.js b/src/components/autosuggest/autosuggest.ui.js
index 1a19883df5..74cce5e8a0 100644
--- a/src/components/autosuggest/autosuggest.ui.js
+++ b/src/components/autosuggest/autosuggest.ui.js
@@ -37,6 +37,7 @@ export default class AutosuggestUI {
errorAPI,
errorAPILinkText,
typeMore,
+ customResultsThreshold,
}) {
// DOM Elements
this.context = context;
@@ -65,6 +66,7 @@ export default class AutosuggestUI {
this.errorAPI = errorAPI || context.getAttribute('data-error-api');
this.errorAPILinkText = errorAPILinkText || context.getAttribute('data-error-api-link-text');
this.typeMore = typeMore || context.getAttribute('data-type-more');
+ this.customResultsThreshold = customResultsThreshold || context.getAttribute('data-result-threshold');
this.language = context.getAttribute('data-lang');
this.allowMultiple = context.getAttribute('data-allow-multiple') || false;
this.listboxId = this.listbox.getAttribute('id');
@@ -293,9 +295,30 @@ export default class AutosuggestUI {
async fetchSuggestions(sanitisedQuery, data) {
this.abortFetch();
- const results = await runFuse(sanitisedQuery, data, this.lang, this.resultLimit);
+
+ const threshold =
+ this.customResultsThreshold != null && this.customResultsThreshold >= 0 && this.customResultsThreshold <= 1
+ ? this.customResultsThreshold
+ : 0.2;
+
+ let distance;
+ if (threshold >= 0.6) {
+ distance = 500;
+ } else if (threshold >= 0.4) {
+ distance = 300;
+ } else {
+ distance = 100;
+ }
+
+ const results = await runFuse(sanitisedQuery, data, this.lang, threshold, distance);
+
results.forEach((result) => {
- result.sanitisedText = sanitiseAutosuggestText(result[this.lang], this.sanitisedQueryReplaceChars);
+ const resultItem = result.item ?? result;
+
+ result.sanitisedText = sanitiseAutosuggestText(
+ resultItem[this.lang] ?? resultItem['formattedAddress'],
+ this.sanitisedQueryReplaceChars,
+ );
});
return {
status: this.responseStatus,
@@ -345,16 +368,18 @@ export default class AutosuggestUI {
this.listbox.innerHTML = '';
if (this.results) {
this.resultOptions = this.results.map((result, index) => {
- let innerHTML = this.emboldenMatch(result[this.lang], this.query);
+ const resultItem = result.item ?? result;
+
+ let innerHTML = this.emboldenMatch(resultItem[this.lang] ?? resultItem['formattedAddress'], this.query);
const listElement = document.createElement('li');
listElement.className = classAutosuggestOption;
listElement.setAttribute('id', `${this.listboxId}__option--${index}`);
listElement.setAttribute('role', 'option');
- if (result.category) {
+ if (resultItem.category) {
innerHTML =
innerHTML +
- `${result.category}`;
+ `${resultItem.category}`;
}
listElement.innerHTML = innerHTML;
listElement.addEventListener('click', () => {
@@ -485,16 +510,19 @@ export default class AutosuggestUI {
if (this.results.length) {
this.settingResult = true;
const result = this.results[index || this.highlightedResultIndex || 0];
+ const resultItem = result.item ?? result;
+ const resultValue = resultItem[this.lang] ?? resultItem['formattedAddress'];
+
this.resultSelected = true;
if (this.allowMultiple === 'true') {
- let value = this.storeExistingSelections(result[this.lang]);
+ let value = this.storeExistingSelections(resultValue);
result.displayText = value;
- } else if (result.url) {
- result.displayText = result[this.lang];
- window.location = result.url;
+ } else if (resultItem.url) {
+ result.displayText = resultValue;
+ window.location = resultItem.url;
} else {
- result.displayText = result[this.lang];
+ result.displayText = resultValue;
}
this.onSelect(result).then(() => (this.settingResult = false));
diff --git a/src/components/autosuggest/example-autosuggest-country.njk b/src/components/autosuggest/example-autosuggest-country.njk
index ddf071ddb0..3358178ad1 100644
--- a/src/components/autosuggest/example-autosuggest-country.njk
+++ b/src/components/autosuggest/example-autosuggest-country.njk
@@ -24,7 +24,8 @@
"resultsTitleId": "country-of-birth-suggestions",
"autosuggestData": "/examples/data/country-of-birth.json",
"noResults": "No suggestions found. You can enter your own answer",
- "typeMore": "Continue entering to get suggestions"
+ "typeMore": "Continue entering to get suggestions",
+ "resultsThreshold": 0.2
})
}}
diff --git a/src/components/autosuggest/fuse-config.js b/src/components/autosuggest/fuse-config.js
index 2b7724a47e..70dade58b6 100644
--- a/src/components/autosuggest/fuse-config.js
+++ b/src/components/autosuggest/fuse-config.js
@@ -1,14 +1,19 @@
import Fuse from 'fuse.js';
-export default function runFuse(query, data, searchFields) {
+export default function runFuse(query, data, searchFields, threshold, distance) {
const options = {
shouldSort: true,
- threshold: 0.2,
+ threshold: threshold,
+ distance: distance,
keys: [
{
name: searchFields,
weight: 0.9,
},
+ {
+ name: 'formattedAddress',
+ weight: 0.9,
+ },
{
name: 'tags',
weight: 0.1,
diff --git a/src/js/analytics.js b/src/js/analytics.js
index 1e6ac1cd86..3bc1d3dad7 100644
--- a/src/js/analytics.js
+++ b/src/js/analytics.js
@@ -44,7 +44,7 @@ export default function initAnalytics() {
document.body.addEventListener('click', ({ target }) => {
if (target.getAttribute('data-ga') === 'click') {
return trackElement(target, 'click');
- } else if (target.parentElement.getAttribute('data-ga') === 'click') {
+ } else if (target.parentElement?.getAttribute('data-ga') === 'click') {
return trackElement(target.parentElement, 'click');
}
});
diff --git a/yarn.lock b/yarn.lock
index 208423613e..56930019ef 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5655,10 +5655,10 @@ functions-have-names@^1.2.3:
resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834"
integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==
-fuse.js@^3.6.1:
- version "3.6.1"
- resolved "https://registry.yarnpkg.com/fuse.js/-/fuse.js-3.6.1.tgz#7de85fdd6e1b3377c23ce010892656385fd9b10c"
- integrity sha512-hT9yh/tiinkmirKrlv4KWOjztdoZo1mx9Qh4KvWqC7isoXwdUY3PNWUxceF4/qO9R6riA2C29jdTOeQOIROjgw==
+fuse.js@^7.0.0:
+ version "7.0.0"
+ resolved "https://registry.yarnpkg.com/fuse.js/-/fuse.js-7.0.0.tgz#6573c9fcd4c8268e403b4fc7d7131ffcf99a9eb2"
+ integrity sha512-14F4hBIxqKvD4Zz/XjDc3y94mNZN6pRv3U13Udo0lNLCWRBUsrMv2xwcF/y/Z5sV6+FQW+/ow68cHpm4sunt8Q==
gensync@^1.0.0-beta.2:
version "1.0.0-beta.2"
@@ -11308,7 +11308,7 @@ string-length@^4.0.1:
char-regex "^1.0.2"
strip-ansi "^6.0.0"
-"string-width-cjs@npm:string-width@^4.2.0":
+"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@@ -11326,15 +11326,6 @@ string-width@^1.0.1, string-width@^1.0.2:
is-fullwidth-code-point "^1.0.0"
strip-ansi "^3.0.0"
-string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
- version "4.2.3"
- resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
- integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
- dependencies:
- emoji-regex "^8.0.0"
- is-fullwidth-code-point "^3.0.0"
- strip-ansi "^6.0.1"
-
string-width@^5.0.1, string-width@^5.1.2:
version "5.1.2"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794"
@@ -11403,7 +11394,7 @@ string_decoder@~1.1.1:
dependencies:
safe-buffer "~5.1.0"
-"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
+"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
@@ -11424,13 +11415,6 @@ strip-ansi@^4.0.0:
dependencies:
ansi-regex "^3.0.0"
-strip-ansi@^6.0.0, strip-ansi@^6.0.1:
- version "6.0.1"
- resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
- integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
- dependencies:
- ansi-regex "^5.0.1"
-
strip-ansi@^7.0.0, strip-ansi@^7.0.1, strip-ansi@^7.1.0:
version "7.1.0"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45"
@@ -12811,7 +12795,7 @@ which@^2.0.1:
dependencies:
isexe "^2.0.0"
-"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
+"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
@@ -12828,15 +12812,6 @@ wrap-ansi@^2.0.0:
string-width "^1.0.1"
strip-ansi "^3.0.1"
-wrap-ansi@^7.0.0:
- version "7.0.0"
- resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
- integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
- dependencies:
- ansi-styles "^4.0.0"
- string-width "^4.1.0"
- strip-ansi "^6.0.0"
-
wrap-ansi@^8.1.0:
version "8.1.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214"