From 3719479356e2fe208643f896e91a8d2b7f5f708c Mon Sep 17 00:00:00 2001 From: Tom Byers Date: Wed, 16 Sep 2020 14:46:49 +0100 Subject: [PATCH 1/2] Add tests for live-search announcements --- tests/javascripts/liveSearch.test.js | 57 ++++++++++++++++++++++++++-- 1 file changed, 54 insertions(+), 3 deletions(-) diff --git a/tests/javascripts/liveSearch.test.js b/tests/javascripts/liveSearch.test.js index 94fc99c94..ba7422b92 100644 --- a/tests/javascripts/liveSearch.test.js +++ b/tests/javascripts/liveSearch.test.js @@ -10,11 +10,25 @@ afterAll(() => { describe('Live search', () => { + let searchLabelText; let searchTextbox; + let liveRegion let list; + function liveRegionResults (num) { + if (num === 1) { + return "1 result"; + } else if (num === 0) { + return "no results"; + } else { + return `${num} results`; + } + }; + describe("With a list of radios", () => { + searchLabelText = "Search branding styles by name"; + function getRadiosHTML (departments) { let result = ''; @@ -62,9 +76,10 @@ describe('Live search', () => {
@@ -72,6 +87,7 @@ describe('Live search', () => {
`; searchTextbox = document.getElementById('search'); + liveRegion = document.querySelector('.live-search__status'); list = document.querySelector('form'); }); @@ -87,6 +103,7 @@ describe('Live search', () => { const listItemsShowing = Array.from(listItems).filter(item => window.getComputedStyle(item).display !== 'none'); expect(listItemsShowing.length).toEqual(listItems.length); + expect(searchTextbox.hasAttribute('aria-label')).toBe(false); }); @@ -101,6 +118,8 @@ describe('Live search', () => { const listItemsShowing = Array.from(listItems).filter(item => window.getComputedStyle(item).display !== 'none'); expect(listItemsShowing.length).toEqual(2); + expect(searchTextbox.hasAttribute('aria-label')).toBe(true); + expect(searchTextbox.getAttribute('aria-label')).toEqual(`${searchLabelText}, ${liveRegionResults(2)}`); }); @@ -115,6 +134,8 @@ describe('Live search', () => { const listItemsShowing = Array.from(listItems).filter(item => window.getComputedStyle(item).display !== 'none'); expect(listItemsShowing.length).toEqual(1); + expect(searchTextbox.hasAttribute('aria-label')).toBe(true); + expect(searchTextbox.getAttribute('aria-label')).toEqual(`${searchLabelText}, ${liveRegionResults(1)}`); }); @@ -152,6 +173,7 @@ describe('Live search', () => { const listItemsShowing = Array.from(listItems).filter(item => window.getComputedStyle(item).display !== 'none'); expect(listItemsShowing.length).toEqual(listItems.length); + expect(liveRegion.textContent.trim()).toEqual(liveRegionResults(listItemsShowing.length)); }); @@ -170,6 +192,7 @@ describe('Live search', () => { const listItemsShowing = Array.from(listItems).filter(item => window.getComputedStyle(item).display !== 'none'); expect(listItemsShowing.length).toEqual(1); + expect(liveRegion.textContent.trim()).toEqual(liveRegionResults(1)); }); @@ -188,6 +211,7 @@ describe('Live search', () => { const listItemsShowing = Array.from(listItems).filter(item => window.getComputedStyle(item).display !== 'none'); expect(listItemsShowing.length).toEqual(2); + expect(liveRegion.textContent.trim()).toEqual(liveRegionResults(2)); }); @@ -216,6 +240,8 @@ describe('Live search', () => { describe("With a list of checkboxes", () => { + searchLabelText = "Search branding styles by name"; + beforeEach(() => { const templatesAndFolders = [ @@ -246,9 +272,10 @@ describe('Live search', () => {
@@ -258,6 +285,7 @@ describe('Live search', () => {
`; searchTextbox = document.getElementById('search'); + liveRegion = document.querySelector('.live-search__status'); list = document.querySelector('form'); }); @@ -273,6 +301,7 @@ describe('Live search', () => { const listItemsShowing = Array.from(listItems).filter(item => window.getComputedStyle(item).display !== 'none'); expect(listItemsShowing.length).toEqual(listItems.length); + expect(searchTextbox.hasAttribute('aria-label')).toBe(false); }); @@ -288,6 +317,8 @@ describe('Live search', () => { // should match 'New patient' and 'New doctor' expect(listItemsShowing.length).toEqual(2); + expect(searchTextbox.hasAttribute('aria-label')).toBe(true); + expect(searchTextbox.getAttribute('aria-label')).toEqual(`${searchLabelText}, ${liveRegionResults(2)}`); }); @@ -302,6 +333,8 @@ describe('Live search', () => { const listItemsShowing = Array.from(listItems).filter(item => window.getComputedStyle(item).display !== 'none'); expect(listItemsShowing.length).toEqual(1); + expect(searchTextbox.hasAttribute('aria-label')).toBe(true); + expect(searchTextbox.getAttribute('aria-label')).toEqual(`${searchLabelText}, ${liveRegionResults(1)}`); }); @@ -334,6 +367,7 @@ describe('Live search', () => { // 2 items contain the "Email template" text // only the text containing the name of the item is matched against (ie 'New patient') expect(listItemsShowing.length).toEqual(0); + expect(searchTextbox.getAttribute('aria-label')).toEqual(`${searchLabelText}, ${liveRegionResults(0)}`); }); @@ -356,6 +390,7 @@ describe('Live search', () => { const listItemsShowing = Array.from(listItems).filter(item => window.getComputedStyle(item).display !== 'none'); expect(listItemsShowing.length).toEqual(listItems.length); + expect(liveRegion.textContent.trim()).toEqual(liveRegionResults(listItemsShowing.length)); }); @@ -374,6 +409,7 @@ describe('Live search', () => { const listItemsShowing = Array.from(listItems).filter(item => window.getComputedStyle(item).display !== 'none'); expect(listItemsShowing.length).toEqual(1); + expect(liveRegion.textContent.trim()).toEqual(liveRegionResults(1)); }); @@ -392,6 +428,7 @@ describe('Live search', () => { const listItemsShowing = Array.from(listItems).filter(item => window.getComputedStyle(item).display !== 'none'); expect(listItemsShowing.length).toEqual(1); + expect(liveRegion.textContent.trim()).toEqual(liveRegionResults(1)); }); @@ -432,6 +469,7 @@ describe('Live search', () => { // 2 items contain the "Email template" text // only the text containing the name of the item is matched against (ie 'New patient') expect(listItemsShowing.length).toEqual(0); + expect(liveRegion.textContent.trim()).toEqual(liveRegionResults(0)); }); @@ -441,6 +479,8 @@ describe('Live search', () => { describe("With a list of content items", () => { + searchLabelText = "Search by name or email address"; + function getContentItems (users) { function getPermissionsHTML (permissions) { @@ -523,9 +563,10 @@ describe('Live search', () => {
@@ -533,6 +574,7 @@ describe('Live search', () => {
`; searchTextbox = document.getElementById('search'); + liveRegion = document.querySelector('.live-search__status'); list = document.querySelector('form'); }); @@ -548,6 +590,7 @@ describe('Live search', () => { const listItemsShowing = Array.from(listItems).filter(item => window.getComputedStyle(item).display !== 'none'); expect(listItemsShowing.length).toEqual(listItems.length); + expect(searchTextbox.hasAttribute('aria-label')).toBe(false); }); @@ -562,6 +605,8 @@ describe('Live search', () => { const listItemsShowing = Array.from(listItems).filter(item => window.getComputedStyle(item).display !== 'none'); expect(listItemsShowing.length).toEqual(1); + expect(searchTextbox.hasAttribute('aria-label')).toBe(true); + expect(searchTextbox.getAttribute('aria-label')).toEqual(`${searchLabelText}, ${liveRegionResults(1)}`); }); @@ -576,6 +621,8 @@ describe('Live search', () => { const listItemsShowing = Array.from(listItems).filter(item => window.getComputedStyle(item).display !== 'none'); expect(listItemsShowing.length).toEqual(1); + expect(searchTextbox.hasAttribute('aria-label')).toBe(true); + expect(searchTextbox.getAttribute('aria-label')).toEqual(`${searchLabelText}, ${liveRegionResults(1)}`); }); @@ -592,6 +639,7 @@ describe('Live search', () => { // all items contain the "Add and edit templates" permission so would match if all text was matched against the search term // only the text containing the label and email address is matched against expect(listItemsShowing.length).toEqual(0); + expect(searchTextbox.getAttribute('aria-label')).toEqual(`${searchLabelText}, ${liveRegionResults(0)}`); }); @@ -632,6 +680,7 @@ describe('Live search', () => { const listItemsShowing = Array.from(listItems).filter(item => window.getComputedStyle(item).display !== 'none'); expect(listItemsShowing.length).toEqual(1); + expect(liveRegion.textContent.trim()).toEqual(liveRegionResults(1)); }); @@ -650,6 +699,7 @@ describe('Live search', () => { const listItemsShowing = Array.from(listItems).filter(item => window.getComputedStyle(item).display !== 'none'); expect(listItemsShowing.length).toEqual(1); + expect(liveRegion.textContent.trim()).toEqual(liveRegionResults(1)); }); @@ -670,6 +720,7 @@ describe('Live search', () => { // all items contain the "Add and edit templates" permission so would match if all text was matched against the search term // only the text containing the label and email address is matched against expect(listItemsShowing.length).toEqual(0); + expect(liveRegion.textContent.trim()).toEqual(liveRegionResults(0)); }); From 43935457d87fc95ebac7eb1b90cfb56a8999c743 Mon Sep 17 00:00:00 2001 From: Tom Byers Date: Wed, 16 Sep 2020 22:01:36 +0100 Subject: [PATCH 2/2] Change live-search so it announces results If there is a search term when the page loads, a statement about the results is added to the searchbox label. All subsequent searches are announced via an aria live-region. --- app/assets/javascripts/liveSearch.js | 36 ++++++++++++++++++++--- app/templates/components/live-search.html | 1 + 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/app/assets/javascripts/liveSearch.js b/app/assets/javascripts/liveSearch.js index 25cf880e5..024de40e9 100644 --- a/app/assets/javascripts/liveSearch.js +++ b/app/assets/javascripts/liveSearch.js @@ -1,32 +1,54 @@ (function(Modules) { "use strict"; + let state; let normalize = (string) => string.toLowerCase().replace(/ /g,''); + let resultsSummary = (num) => { + if (num === 0) { + return "no results"; + } else { + return num + (num === 1 ? " result" : " results"); + } + }; - let filter = ($searchBox, $targets) => () => { + let filter = ($searchBox, $searchLabel, $liveRegion, $targets) => () => { let query = normalize($searchBox.val()); + let results = 0; $targets.each(function() { let content = $('.live-search-relevant', this).text() || $(this).text(); + let isMatch = normalize(content).indexOf(normalize(query)) > -1; if ($(this).has(':checked').length) { $(this).show(); + results++; return; } if (query == '') { $(this).css('display', ''); + results++; return; } - $(this).toggle( - normalize(content).indexOf(normalize(query)) > -1 - ); + $(this).toggle(isMatch); + + if (isMatch) { results++; } }); + if (state === 'loaded') { + if (query !== '') { + $searchBox.attr('aria-label', $searchLabel.text().trim() + ', ' + resultsSummary(results)); + } + state = 'active'; + } else { + $searchBox.removeAttr('aria-label'); + $liveRegion.text(resultsSummary(results)); + } + // make sticky JS recalculate its cache of the element's position // because live search can change the height document if ('stickAtBottomWhenScrolling' in GOVUK) { @@ -43,12 +65,18 @@ let $component = $(component); let $searchBox = $('input', $component); + let $searchLabel = $('label', $component); + let $liveRegion = $('.live-search__status', $component); let filterFunc = filter( $searchBox, + $searchLabel, + $liveRegion, $($component.data('targets')) ); + state = 'loaded'; + $searchBox.on('keyup input', filterFunc); filterFunc(); diff --git a/app/templates/components/live-search.html b/app/templates/components/live-search.html index 6ba673350..4108ded7f 100644 --- a/app/templates/components/live-search.html +++ b/app/templates/components/live-search.html @@ -14,6 +14,7 @@ "label": {"text": search_label}, "autocomplete": "off" }) }} +
{% endif %}