diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 350d2a9ca..d18f212fc 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -10,6 +10,7 @@ on: branches: [ "master", + "feature/DM-4866-search-results-refresh", "ci-migration", "ci-migration-cv", "ci-migration-en", @@ -17,7 +18,7 @@ on: "ci-migration-bj", ] pull_request: - branches: ["master"] + branches: ["master", "feature/DM-4866-search-results-refresh"] jobs: build: name: rspec diff --git a/app/assets/javascripts/_practice_card_utilities.es6 b/app/assets/javascripts/_practice_card_utilities.es6 index f1b0152c8..e0911e7b3 100644 --- a/app/assets/javascripts/_practice_card_utilities.es6 +++ b/app/assets/javascripts/_practice_card_utilities.es6 @@ -41,11 +41,9 @@ function toggleFocusStylingForPracticeTitle() { function replaceImagePlaceholders() { $('.dm-practice-card').each(function() { const placeholder = $(this).find('.practice-card-img-placeholder'); - const practiceId = placeholder.attr('data-practice-id'); const imagePath = placeholder.attr('data-practice-image'); const practiceName = placeholder.attr('data-practice-name'); - if (practiceId && imagePath) { fetchSignedResource(imagePath).then(signedUrl => { replacePlaceholderWithImage(signedUrl, practiceId, practiceName); @@ -74,6 +72,48 @@ function replacePlaceholderWithImage(imageUrl, practiceId, practiceName) { }); } +function replaceSearchPageImagePlaceholders(practiceEls) { + $('.dm-search-result').each(function() { + const $resultElement = $(this); + const placeholder = $resultElement.find('.search-result-img-container'); + + if (placeholder.find('img').length > 0) { + return; + } + + const practiceId = placeholder.attr('data-practice-id'); + const imagePath = placeholder.attr('data-practice-image'); + const practiceName = placeholder.attr('data-practice-name'); + + if (practiceId && imagePath) { + fetchSignedResource(imagePath).then(signedUrl => { + + replaceSearchResultPlaceholderImage(signedUrl, practiceId, practiceName).then(updatedElement => { + practiceEls[practiceId] = updatedElement; + }); + }); + } + }); +} + +function replaceSearchResultPlaceholderImage(imageUrl, practiceId, practiceName) { + return new Promise((resolve, reject) => { + loadImage(imageUrl, function(loadedImageSrc) { + const imgElement = $('') + .attr('data-resource-id', practiceId) + .attr('src', loadedImageSrc) + .attr('alt', '') + .addClass('search-result-img'); + + const placeholder = $('.search-result-img-container[data-practice-id="' + practiceId + '"]'); + placeholder.empty().append(imgElement); + placeholder.addClass('loaded-image'); + + resolve(placeholder.closest('.dm-search-result').get(0).outerHTML); + }); + }); +} + function loadImage(imageSrc, callback) { var img = new Image(); img.onload = function() { diff --git a/app/assets/stylesheets/dm/components/_search_result.scss b/app/assets/stylesheets/dm/components/_search_result.scss new file mode 100644 index 000000000..aa39940ef --- /dev/null +++ b/app/assets/stylesheets/dm/components/_search_result.scss @@ -0,0 +1,78 @@ +.dm-search-result { + border-top: 2px solid #e6e6e6 !important; + position: relative; + @include u-display(block); + @include u-width('full'); + @include u-padding-y(4); + + .dm-practice-retired-banner { + position: absolute; + width: 80px; + height: 24px; + justify-content: center; + background-color: color($theme-color-base-light); + padding: 5px; + border-radius: 0px 2px 2px 0px; + z-index: 2; + top: 20px; + left: 12px; + display: flex; + align-items: center; + letter-spacing: 0.05em; + text-transform: uppercase; + color: color($theme-color-base-ink); + font-size: 13px; + line-height: 115%; + } + + .search-result-img-container { + height: 133px; + background-color: color($theme-color-base-lightest); + display: flex; + @include u-margin-bottom(3); + + img { + flex-shrink: 0; + @include at-media(tablet) { + min-height: 100%; + min-width: 100%; + } + } + + h3 { + align-self: flex-end; + color: color($theme-color-dm-white); + } + + @include at-media(tablet) { + flex: 0 0 220px; + margin-right: 30px; + margin-bottom: unset; + } + } + + .search-result-img-container.loaded-image { + background-color: unset; + } + + .search-result-description { + @include grid-col(12); + + h3 { + @include u-margin-top(0); + @include u-margin-bottom(2); + @include u-text('normal'); + } + + .innovation-tagline { + display: block; + @include u-height(10); + overflow: hidden; + } + + .originating-facility { + @include u-font('sans','2xs'); + @include u-text('italic'); + } + } +} \ No newline at end of file diff --git a/app/assets/stylesheets/dm/pages/_search.scss b/app/assets/stylesheets/dm/pages/_search.scss index a39f63826..c7585ffce 100644 --- a/app/assets/stylesheets/dm/pages/_search.scss +++ b/app/assets/stylesheets/dm/pages/_search.scss @@ -1,10 +1,59 @@ // Styles for the practice search page #search-page { - .usa-checkbox__label:before { - border-radius: 0 !important; + .applied-filter { + @extend .usa-tag; + @extend .usa-tag--big; + display: block; + @include at-media(tablet) { + display: inline-block; + } + } + + .applied-filter-1 { + @include transition-btn-colors; + } + + .applied-filter-2 { + background-color: color("blue-10"); + color: color("blue-60"); + + &:hover { + background-color: color("blue-20"); + } + + &:active { + background-color: color("blue-40"); + } + } + + .applied-filter-3 { + background-color: color("gray-60"); + color: white; + + &:hover { + background-color: color("gray-50"); + } + + &:active { + background-color: color("gray-80"); + } + } + + .applied-filter-4 { + background-color: color("gray-30"); + color: black; + + &:hover { + background-color: color("gray-20"); + } + + &:active { + background-color: color("gray-50"); + } } - .usa-combo-box__toggle-list, .usa-combo-box__clear-input { + .usa-combo-box__toggle-list, + .usa-combo-box__clear-input { top: 2.3em !important; } @@ -23,15 +72,44 @@ .button-box { margin-bottom: 24px; } -} -.dm-search-practices { + .category-filters { + scrollbar-width: thin; + scrollbar-color: rgba(0, 0, 0, 0.2) rgba(0, 0, 0, 0.1); + overflow-y: scroll; + scroll-behavior: smooth; + } + + .category-filters::-webkit-scrollbar { + width: 8px; + } + + .category-filters::-webkit-scrollbar-track { + background: rgba(0, 0, 0, 0.1); + } + + .category-filters::-webkit-scrollbar-thumb { + background-color: rgba(0, 0, 0, 0.2); + border-radius: 10px; + border: 2px solid rgba(0, 0, 0, 0.1); + } + + .category-filters::-webkit-scrollbar-thumb:hover { + background-color: rgba(0, 0, 0, 0.3); + } + + .filter-submit-button { + @include transition-btn-colors; + } + hr { border: none; - height: 2px; + height: 1px; background: #dcdee0; } +} +.dm-search-practices { mark { background: color($theme-color-accent-warm); font-weight: bold; @@ -57,7 +135,7 @@ height: calc(1em * 1.5 * 2); } .three-lines { - -webkit-line-clamp : 3; + -webkit-line-clamp: 3; height: calc(1em * 1.5 * 3); } .four-lines { @@ -73,7 +151,7 @@ /* Adapted from: http://www.mademyday.de/css-height-equals-width-with-pure-css.html */ .img-box { position: relative; - width: 100%; /* desired width */ + width: 100%; /* desired width */ } .img-box-content { @@ -82,11 +160,11 @@ width: 100%; height: 210px; - @media all and (min-width: 640px){ + @media all and (min-width: 640px) { height: 200px; } - @media all and (min-width: 870px){ + @media all and (min-width: 870px) { width: 254px; height: 257px; } @@ -120,25 +198,9 @@ .fa-times { display: block !important; } - - .search-category-filters, .search-originating-facility-filter, .search-adopting-facility-filter { - @include u-margin-bottom(1); - - @media screen and (min-width: 1024px) { - @include u-margin-bottom(0); - } - } - - #update-search-results-button { - @include transition-btn-colors; - } } #update-search-results-form { - #mobile_filters_button { - @include transition-btn-colors; - } - #dm-practice-search-button { @include transition-btn-colors; background-image: none !important; @@ -158,14 +220,9 @@ } } -#close_filters_modal { - background: none !important; - border: 0 !important; - margin: 0 !important; - padding: 0 !important; -} - -.category-container, .originating-facility-container, .adopting-facility-container { +.category-container, +.originating-facility-container, +.adopting-facility-container { background-color: color($theme-color-secondary-light); @media screen and (min-width: 1024px) { @@ -173,9 +230,9 @@ } } -.category-container, { +.category-container { .desktop\:grid-col-4 { - @include u-padding-right('05'); + @include u-padding-right("05"); } div:last-child { .usa-checkbox { @@ -196,22 +253,9 @@ } } -.mobile-modal-container { - position: relative; - padding-bottom: 93px; - min-height: 100vh; - - @media screen and (min-width: 1024px) { - position: unset; - padding-bottom: 0; - min-height: unset; - } -} - #filter_button_container { @media screen and (max-width: 1023px) { - position: absolute; bottom: 0; width: 100%; } -} \ No newline at end of file +} diff --git a/app/views/home/index.html.erb b/app/views/home/index.html.erb index f508e7549..c997298be 100644 --- a/app/views/home/index.html.erb +++ b/app/views/home/index.html.erb @@ -66,7 +66,7 @@ <% end %> - Browse all Tags + Browse all Tags
diff --git a/app/views/practices/_search.js.erb b/app/views/practices/_search.js.erb index 48057a36f..c6830dd15 100644 --- a/app/views/practices/_search.js.erb +++ b/app/views/practices/_search.js.erb @@ -11,17 +11,10 @@ var SPINNER = '.dm-loading-spinner'; var LOAD_MORE_CONTAINER = '.button-box'; var SEARCH_RESULTS = '#search-results'; - var SORT_EL = '#search_sort_option'; + var SORT_EL = 'input[name="search_sort_option"]'; var isBlankSearch = false; var NO_SEARCH_RESULTS_TEXT = '.no-query-results-p'; - // Add Namespace for scroll position - var pagePosition = { - original: 0, - // Log the user's spot on the page when they open the filters modal on mobile - getScrollPosition: function () { - this.original = $(window).scrollTop(); - } - } + var practiceElements = {}; var all_categories = <%= raw @categories.to_json %>; <% end %> @@ -127,6 +120,49 @@ function buildPracticeCard(result) { return cardHtml; } +function buildSearchResult(result) { + var resultHtml = ''; + var practiceImage = result.item.image || 'default-image-placeholder.jpg'; // use a default placeholder if no image is available + var practiceName = result.item.name; + var practiceId = result.item.id; + var practiceTagline = result.item.tagline || ''; + var initiatingFacilityName = result.item.initiating_facility_name || ''; + + var resultHeaderHtml = + '

' + + '' + + practiceName + + '' + + '

'; + + // Default card image placeholder + var resultImagePlaceholder = + '
'; + + var resultRetiredBanner = ''; + if (result.item.retired) { + resultRetiredBanner = '
Retired
'; + } + + resultHtml += `
+
+ ${resultImagePlaceholder} + ${resultRetiredBanner} +
+ ${resultHeaderHtml} +

+ ${practiceTagline} +

+ + ${initiatingFacilityName} + +
+
+
`; + + return resultHtml; +} + function showSpinner() { $(SPINNER).removeClass('display-none'); $(SPINNER).addClass('display-flex'); @@ -157,63 +193,8 @@ function hideResultsElAndShowSpinner() { showSpinner(); } -function openMobileFiltersModal(openOnPageLoad) { - $(document).on('click', '#mobile_filters_button', function() { - $('.search-filters-accordion-button').attr('aria-expanded', 'true'); - $('#search_filters_dropdown').removeAttr('hidden'); - // Return the user to their original spot on the page, before opening the mobile modal - pagePosition.getScrollPosition(); - }); - if (openOnPageLoad) { - $('#mobile_filters_button').trigger('click'); - } -} - -function toggleAccordion(accordionEl, containerEl) { - if (($(accordionEl).attr('aria-expanded') === 'true')) { - $(accordionEl).attr('aria-expanded', 'false'); - $(containerEl).addClass('display-none'); - } else { - $(accordionEl).attr('aria-expanded', 'true'); - $(containerEl).removeClass('display-none'); - } -} - -function toggleMobileCategoriesAccordion(openOnPageLoad) { - $(document).on('click', '.mobile-category-accordion', function() { - toggleAccordion(this, '.category-container'); - }); - - if (openOnPageLoad) { - $('.mobile-category-accordion').each(function() { - toggleAccordion(this, '.category-container'); - }); - } -} - -function toggleMobileOriginAccordion() { - $(document).on('click', '.mobile-origin-accordion', function() { - toggleAccordion(this, '.originating-facility-container'); - }); -} - -function toggleMobileAdoptingAccordion() { - $(document).on('click', '.mobile-adopting-accordion', function() { - toggleAccordion(this, '.adopting-facility-container'); - }); -} - -function closeMobileFiltersModal() { - $(document).on('click', '#close_filters_modal', function () { - $('.search-filters-accordion-button').attr('aria-expanded', 'false') - $('#search_filters_dropdown').attr('hidden', 'true'); - // Bring the user back to their original spot on the page, in case they scrolled down while in the modal - $(window).scrollTop(pagePosition.original); - }); -} - -function searchPracticesPage() { +function searchPracticesPage(skipSearchTracking=false) { const searchField = document.querySelector('#dm-practice-search-field'); // Skip if not the search page if (location.pathname !== '/search') return; @@ -221,14 +202,20 @@ function searchPracticesPage() { // Template out the search results function buildSearchResults(results, sortOption) { $(SPINNER).addClass('display-none'); - var tmpResults = results.sort(sortOption); + var sortedResults = results.sort(sortOption); - var resultsArray = []; var finalResults = ''; - tmpResults.sort(sortByRetired).forEach(function (result) { - const practiceCard = buildPracticeCard(result) + sortedResults.sort(sortByRetired).forEach(function (result, index) { + let practiceCard; + + if (practiceElements[result.item.id]) { + practiceCard = practiceElements[result.item.id]; + } else { + practiceCard = buildSearchResult(result); + practiceElements[result.item.id] = practiceCard; + } + finalResults += practiceCard; - resultsArray.push(practiceCard); '' + '' }); if (results.length > 0) { - - // Hide sorting options if there's only 1 result - if (results.length === 1) { - sortSelect.addClass('display-none'); - } else if (results.length !== 1 && sortSelect.hasClass('display-none')) { - sortSelect.removeClass('display-none'); - } - // Show the results summary section if ($(RESULTS_SECTION).hasClass('display-none')) { $(RESULTS_SECTION).removeClass('display-none'); @@ -254,9 +233,9 @@ function searchPracticesPage() { $(SEARCH_RESULTS).removeClass('display-none'); // Print results to the page - document.querySelector('#search-results').innerHTML = finalResults; + document.querySelector(SEARCH_RESULTS).innerHTML = finalResults; // After the html for the card has been built and applied to the DOM, replace the image 'src' based on the signed url - replaceImagePlaceholders(); + replaceSearchPageImagePlaceholders(practiceElements); $(SEARCH_RESULTS).showMoreItems({ startNum: 12, afterNum: 12, @@ -293,8 +272,11 @@ function searchPracticesPage() { // set the search page search bar input value searchField.value = query; + const searchHeader = document.getElementById('searchHeader'); if (query) { trackSearchField(query); + searchHeader.textContent = `Search Results for: ${query}`; + var searchBarQueryArray = []; search_options.keys.forEach(function(practiceKey) { var queryObj = {}; @@ -302,16 +284,26 @@ function searchPracticesPage() { return searchBarQueryArray.push(queryObj); }); } else { + searchHeader.textContent = "Search"; if (searchLocation[0] === '?filter_by') { trackSearchByFilter(searchLocation[1]); } } + $('.applied-filter').remove(); + $('#reset-search-filters-button').addClass('display-none'); + if (categories) { - // Collect any practice that has a category that matches the selected category/categories. var categoryFilters = []; - categories.forEach(function (categorySelection) { - return categoryFilters.push({'category_names': "=\"" + categorySelection + "\""}); + + Object.entries(categories).forEach(function ([parentCat, subCats]) { + if (subCats.length) { + var catType = parentCat === 'communities' ? 'COMMUNITY' : 'TAG'; + subCats.forEach(function (subCat) { + addAppliedFilter(catType, subCat); + categoryFilters.push({'category_names': "=\"" + subCat + "\""}); + }); + } }); } @@ -335,6 +327,10 @@ function searchPracticesPage() { originFacilityArray.push({'origin_facilities': "=\"" + vf.official_station_name + "\""}); } }); + + // Add applied filter pill for just the Clinical Resource Hub + addAppliedFilter('ORIGIN', visnFacilities[visnFacilities.length - 1].official_station_name) + } else if (originatingFacility && !originatingFacility.includes('VISN-')){ // Collect the practices that have an origin facility matching the selected facility var foundOriginatingFacility = facilities.find(function(f) { @@ -344,6 +340,7 @@ function searchPracticesPage() { return originatingFacility === f.official_station_name; } }); + addAppliedFilter('ORIGIN', foundOriginatingFacility.official_station_name) originFacilityArray.push({'origin_facilities': "=\"" + (foundOriginatingFacility.hasOwnProperty('station_number') ? foundOriginatingFacility.station_number : foundOriginatingFacility.official_station_name) + "\""}); } @@ -379,6 +376,9 @@ function searchPracticesPage() { } }); + // Add applied filter pill for just the Clinical Resource Hub + addAppliedFilter('ADOPTION', filteredFacilities[filteredFacilities.length - 1].official_station_name) + getAdoptionFacilities(stationNumbers, adoptionFacilityArray); } else { // Collect any practice that has a diffusion history with a facility that matches the selected facility @@ -386,6 +386,7 @@ function searchPracticesPage() { foundAdoptingFacility.station_number ? stationNumbers.push(foundAdoptingFacility.station_number) : stationNumbers.push(foundAdoptingFacility.official_station_name) getAdoptionFacilities(stationNumbers, adoptionFacilityArray); + addAppliedFilter('ADOPTION', foundAdoptingFacility.official_station_name) } } @@ -399,12 +400,13 @@ function searchPracticesPage() { the results will have the same score because it stops after the first match. */ function improveResultScoreBasedOnAmountOfCategoryMatches(results) { + var allChosenCategories = Object.values(categories).flat(); results.map(function(result) { var resultScore = result.score; var cat_names = result.item.category_names; if (cat_names && Array.isArray(cat_names)) { cat_names.forEach(function (cn) { - if (categories.includes(cn)) { + if (allChosenCategories.includes(cn)) { resultScore = result.score / 2; } result.score = resultScore; @@ -718,17 +720,15 @@ function searchPracticesPage() { if (!query && !categories && !originatingFacility && !adoptingFacility) { // on an empty search or filtered search, sort by most adoptions and disable the score sorting option, since the score for each result is the same buildSearchResults(results, sortByAdoptionCounts); - $(SORT_EL).children().first().attr('disabled', 'true'); $(SORT_EL).prop('selectedIndex', 2); } else { buildSearchResults(results, sortByScore); // re-enable the score sorting option - $(SORT_EL).children().first().removeAttr('disabled'); $(SORT_EL).prop('selectedIndex', 0); } // Print the number of results for the query - resultsSummary.innerHTML = results.length + ' result' + (results.length === 1 ? ':' : 's:'); + resultsSummary.innerHTML = results.length + ' Result' + (results.length === 1 ? ':' : 's:'); // Highlight results (only Fuse.js matching) results.forEach(function (result) { @@ -736,10 +736,64 @@ function searchPracticesPage() { }); // Highlight search results where exact keyword matches - var mark = new Mark('#search-results'); + var mark = new Mark(SEARCH_RESULTS); mark.mark(query); } + function addAppliedFilter(filterType, filterName) { + const resultsSummary = document.getElementById('results-summary'); + const resetButton = document.getElementById('reset-search-filters-button'); + const appliedFilterName = filterName.toUpperCase(); + const appliedFilterString = `${filterType}: ${appliedFilterName} `; + const appliedFilterButton = document.createElement('button'); + const filterClassIndexMap = { + 'TAG': 1, + 'COMMUNITY': 2, + 'ORIGIN': 3, + 'ADOPTION': 4 + }; + const filterClassIndex = filterClassIndexMap[filterType] || 1; + + appliedFilterButton.className = `usa-button applied-filter applied-filter-${filterClassIndex}`; + appliedFilterButton.id = `${filterName}`; + appliedFilterButton.textContent = appliedFilterString + ' X'; + appliedFilterButton.style.cursor = 'pointer'; + appliedFilterButton.setAttribute( + 'aria-label', + `Remove ${filterType} filter for ${filterName} and update search results` + ); + + appliedFilterButton.addEventListener('click', function(event) { + uncheckFilter(filterType, filterName); + $('#reset-search-filters-button').addClass('display-none'); + updateResults(); + event.stopPropagation(); + }); + + resultsSummary.parentNode.insertBefore(appliedFilterButton, resetButton); + resetButton.classList.remove('display-none'); + } + + function uncheckFilter(filterType, filterName) { + if (filterType === 'TAG' || filterType === 'COMMUNITY') { + const checkbox = Array.from(document.querySelectorAll('.usa-checkbox__input')).find(c => + c.labels && c.labels.length && c.labels[0].textContent.trim() === filterName); + if (checkbox) { + checkbox.checked = false; + } + } else if (filterType === 'ORIGIN') { + const input = document.querySelector('#originating-facility-container input[type="text"]'); + if (input) { + input.value = ''; + } + } else if (filterType === 'ADOPTION') { + const input = document.querySelector('#adopting-facility-container input[type="text"]'); + if (input) { + input.value = ''; + } + } + } + // Highlight Fuse.js results // Adapted from: https://github.com/brunocechet/Fuse.js-with-highlight function highlighter(resultItem) { @@ -827,100 +881,84 @@ function searchPracticesPage() { } } - function closeAccordion(accordionEl, containerEl) { - $(accordionEl).attr('aria-expanded', 'false'); - $(containerEl).addClass('display-none'); - } - - var accordionButton = $('.search-filters-accordion-button') - function accordionText(checkboxCount, inputCount) { - return accordionButton.text('Filters ' + '(' + (checkboxCount + inputCount) + ')'); - } - $(document).on('submit', '#update-search-results-form', function (e) { e.preventDefault(); hideResultsSectionElAndShowSpinner(); + updateResults(); + }); - // Close search filter accordion if it's open - if (accordionButton.attr('aria-expanded') == 'true') { - accordionButton.attr('aria-expanded', 'false'); - $('#search_filters_dropdown').attr('hidden', 'true'); + function updateResults(checkFacilityInputs = true) { + const searchFieldValue = searchField.value.trim(); + if (searchFieldValue !== '') { + window.history.pushState({ turbolinks: {} }, "", `/search?query=${searchFieldValue}`); + } + let originatingFacility = null; + let adoptingFacility = null; + + if (checkFacilityInputs) { + originatingFacility = getFacilityValue( + '#originating-facility-and-visn-select', + '#originating-facility-and-visn-select--list > li[aria-selected=true]' + ); + + adoptingFacility = getFacilityValue( + '#adopting-facility-and-visn-select', + '#adopting-facility-and-visn-select--list > li[aria-selected=true]' + ); } - // Close mobile accordions - closeAccordion('.mobile-origin-accordion', '.originating-facility-container'); - closeAccordion('.mobile-adopting-accordion', '.adopting-facility-container'); - closeAccordion('.mobile-category-accordion', '.category-container'); + const { chosenCategoriesByParent, allCategories } = getSelectedCategories(); - // Bring the user back to their original spot on the page, before they opened the mobile modal - if ($(window).width() < 1024) { - $(window).scrollTop(pagePosition.original); + if (!skipSearchTracking) { + updateCategoryUsage(searchFieldValue, allCategories); } - var urlQuery = searchField.value === '' ? '/search' : '/search?query=' + searchField.value; - window.history.pushState({ turbolinks: {} }, "", urlQuery); + search(searchFieldValue, chosenCategoriesByParent, originatingFacility, adoptingFacility); + } - // Get the value of the originating facility combo box - var originatingFacilityInput = $('#originating-facility-and-visn-select'); - var originatingFacilityLi = $('#originating-facility-and-visn-select--list > li[aria-selected=true]'); + function getFacilityValue(inputSelector, listItemSelector) { + const facilityInput = $(inputSelector); + const facilityLi = $(listItemSelector); + return getComboBoxVal(facilityInput, facilityLi); + } - var originatingFacility = getComboBoxVal(originatingFacilityInput, originatingFacilityLi); + function getSelectedCategories() { + const selectedCategoryCheckboxes = $('#search-filters [type="checkbox"]:checked:not(".all-clinical-checkbox"):not(".all-operational-checkbox"):not(".all-strategic-checkbox"):not(".all-communities-checkbox")'); - // Get the value of the adopting facility combo box - var adoptingFacilityInput = $('#adopting-facility-and-visn-select'); - var adoptingFacilityLi = $('#adopting-facility-and-visn-select--list > li[aria-selected=true]'); + const chosenCategoriesByParent = {}; + const allCategories = []; - var adoptingFacility = getComboBoxVal(adoptingFacilityInput, adoptingFacilityLi); + selectedCategoryCheckboxes.each(function () { + const parentCategory = this.dataset.parentCategory; + const value = this.value; - // Get the values for any chosen categories - var selectedCategoryCheckboxes = $('#update-search-results-form [type="checkbox"]:checked:not(".all-clinical-checkbox"):not(".all-operational-checkbox"):not(".all-strategic-checkbox"):not(".all-communities-checkbox")'); - var chosenCategories = selectedCategoryCheckboxes.map(function () { - return this.value; - }).get(); - if (!chosenCategories.length) { - chosenCategories = null; - } + if (!chosenCategoriesByParent[parentCategory]) { + chosenCategoriesByParent[parentCategory] = []; + } - // Update Category Usage.... + chosenCategoriesByParent[parentCategory].push(value); + allCategories.push(value); + }); + + return { + chosenCategoriesByParent: Object.keys(chosenCategoriesByParent).length === 0 ? null : chosenCategoriesByParent, + allCategories + }; + } + + function updateCategoryUsage(query, chosenCategories) { Rails.ajax({ type: 'patch', url: "/update_category_usage", - data: jQuery.param({query: searchField.value, chosenCategories: chosenCategories}) + data: jQuery.param({ query, chosenCategories }) }); - - // Run the search - search(searchField.value, chosenCategories, originatingFacility, adoptingFacility); - - // Display the amount of filters used(if any) - var checkboxCount = selectedCategoryCheckboxes.length; - var noOriginatingFacilityText = originatingFacilityInput.val() === ''; - var originatingFacilityTextPresent = originatingFacilityInput.val() !== ''; - var noAdoptingFacilityText = adoptingFacilityInput.val() === ''; - var adoptingFacilityTextPresent = adoptingFacilityInput.val() !== ''; - - if (checkboxCount > 0 && originatingFacilityTextPresent && noAdoptingFacilityText) { - accordionText(checkboxCount, 1); - } else if (checkboxCount > 0 && noOriginatingFacilityText && adoptingFacilityTextPresent) { - accordionText(checkboxCount, 1); - } else if (checkboxCount > 0 && noOriginatingFacilityText && noAdoptingFacilityText) { - accordionText(checkboxCount, 0); - } else if (checkboxCount > 0 && originatingFacilityTextPresent && adoptingFacilityTextPresent) { - accordionText(checkboxCount, 2); - } else if (checkboxCount === 0 && originatingFacilityTextPresent && noAdoptingFacilityText) { - accordionText(0, 1); - } else if (checkboxCount === 0 && noOriginatingFacilityText && adoptingFacilityTextPresent) { - accordionText(0, 1); - } else if (checkboxCount === 0 && noOriginatingFacilityText && noAdoptingFacilityText) { - return accordionButton.text('Filters'); - } else if (checkboxCount === 0 && originatingFacilityTextPresent && adoptingFacilityTextPresent) { - accordionText(0, 2); - } - }); + } // Sort the results based on each sort option var sortSelect = $(SORT_EL); sortSelect.on('change', function() { - switch ($(this).val()) { + var selectedValue = $(SORT_EL + ':checked').val(); + switch (selectedValue) { case "most_relevant": hideResultsElAndShowSpinner(); buildSearchResults(results, sortByScore); @@ -941,22 +979,28 @@ function searchPracticesPage() { }); // Clear all filter options - $(document).on('click', '#resetSearchFiltersButton, #resetSearchFiltersButtonMobile', function(e) { + $(document).on('click', '#reset-search-filters-button', function(e) { e.preventDefault(); - $('.usa-combo-box__clear-input').each(function() { - $(this).trigger('click'); - }); + const originInput = document.querySelector('#originating-facility-container input.usa-combo-box__input'); + const adoptionInput = document.querySelector('#adopting-facility-container input.usa-combo-box__input'); + + if (originInput) { + originInput.value = ''; + } + + if (adoptionInput) { + adoptionInput.value = ''; + } + $('.usa-checkbox__input').each(function() { if ($(this).prop('checked')) { $(this).prop('checked', false); } }); - // Move the focus area back to the top of the filters window - $(".usa-combo-box__clear-input__wrapper").attr("tabindex",-1).focus(); - // Reset filter count text - $('.search-filters-accordion-button').text('Filters'); + + searchPracticesPage(); }) if (window.location.pathname === '/search' && window.location.search !== '' && searchLocation[0] !== '?filter_by') { @@ -970,34 +1014,17 @@ function searchPracticesPage() { const checkboxSelector = `input[type="checkbox"][name="${category}"]`; const checkbox = $(checkboxSelector); checkbox.prop('checked', true); - accordionText(1, 0) - search('', [category]); - } else if (query) { - search(decodeURI(query)); - searchField.value = decodeURI(query); } else if (allCommunities) { let allCommunitiesCheckbox = document.querySelector('.all-communities-checkbox'); if (allCommunitiesCheckbox) { allCommunitiesCheckbox.checked = true; $(allCommunitiesCheckbox).trigger('change'); } - - let selectedCategoryCheckboxes = $('#update-search-results-form [type="checkbox"]:checked:not(".all-clinical-checkbox"):not(".all-operational-checkbox"):not(".all-strategic-checkbox"):not(".all-communities-checkbox")'); - let chosenCategories = selectedCategoryCheckboxes.map(function () { - return this.value; - }).get(); - - if (!chosenCategories.length) { - chosenCategories = null; - } else { - accordionText(chosenCategories.length, 0) - } - search('', chosenCategories); - } else { - // No query or category filter - searchField.value = ''; - search(''); - } + } else if (query) { + searchField.value = decodeURI(query); + }; + // update search results but skip checking Location comboboxes + updateResults(false); } else if ((searchLocation[0] === '?filter_by') || (window.location.pathname === '/search' && window.location.search === '')) { hideResultsSectionElAndShowSpinner(); search(''); @@ -1015,12 +1042,6 @@ function trackSearchByFilter(filterTerm) { ahoy.track('Practice search by filter', {filter_term: decodedFilterTerm}); } -function preventFiltersAccordionContentFlickerOnPageLoad() { - $(document).arrive('.desktop-filters-accordion-heading', { existing: true }, (el) => { - $(el).next().removeClass('display-none'); - }); -} - function addAllCheckBoxListener(allCheckboxSelector, standardCheckboxSelector) { let allCheckbox = allCheckboxSelector; let checkbox = standardCheckboxSelector; @@ -1043,23 +1064,68 @@ function addAllCheckBoxListener(allCheckboxSelector, standardCheckboxSelector) { }); } +function moveFilters() { + const width = window.innerWidth; + const filtersContainer = document.getElementById('search-filters'); + const mobileContainer = document.getElementById('mobile-filters-container'); + const desktopContainer = document.getElementById('desktop-filters-container'); + + if (!filtersContainer || !mobileContainer || !desktopContainer) { + return; + } + + if (width < 1024) { + if (desktopContainer.contains(filtersContainer)) { + desktopContainer.removeChild(filtersContainer); + } + if (!mobileContainer.contains(filtersContainer)) { + mobileContainer.appendChild(filtersContainer); + } + } else { + if (mobileContainer.contains(filtersContainer)) { + mobileContainer.removeChild(filtersContainer); + } + if (!desktopContainer.contains(filtersContainer)) { + desktopContainer.appendChild(filtersContainer); + } + } + filtersContainer.style.display = 'block'; +} + +function setupSkipToFirstResult() { + const skipDiv = document.getElementById('skip-tag-filters'); + + if (skipDiv) { + skipDiv.addEventListener('focus', function() { + document.addEventListener('keydown', function(event) { + if (event.key === 'Enter' || event.key === ' ') { + event.preventDefault(); + + const firstResultLink = document.querySelector('.dm-link-title'); + if (firstResultLink) { + firstResultLink.focus(); + } + } + }, { once: true }); + }); + } +}; + function execSearchFunctions() { const urlParams = new URLSearchParams(window.location.search); - const openFiltersOnPageLoad = urlParams.get('filters') === 'open'; - - openMobileFiltersModal(openFiltersOnPageLoad); - toggleMobileCategoriesAccordion(openFiltersOnPageLoad); - toggleMobileOriginAccordion(); - toggleMobileAdoptingAccordion(); - closeMobileFiltersModal(); - preventFiltersAccordionContentFlickerOnPageLoad(); + const skipSearchTracking = urlParams.has('all_communities') || urlParams.has('category');; + addAllCheckBoxListener('.all-clinical-checkbox', '.clinical-checkbox'); addAllCheckBoxListener('.all-operational-checkbox', '.operational-checkbox'); addAllCheckBoxListener('.all-strategic-checkbox', '.strategic-checkbox'); addAllCheckBoxListener('.all-communities-checkbox', '.communities-checkbox'); - searchPracticesPage(); + searchPracticesPage(skipSearchTracking); + moveFilters(); + setupSkipToFirstResult(); } +window.addEventListener('resize', moveFilters); + document.addEventListener('turbolinks:load', function () { execSearchFunctions(); }); diff --git a/app/views/practices/search.html.erb b/app/views/practices/search.html.erb index d60f6321d..2ec82ae48 100644 --- a/app/views/practices/search.html.erb +++ b/app/views/practices/search.html.erb @@ -14,73 +14,69 @@ <% end %>
-
+
<%= render partial: "shared/messages", locals: {small_text: false} %> -
- -
- - -
- diff --git a/app/views/practices/search_partials/_category_checkbox.html.erb b/app/views/practices/search_partials/_category_checkbox.html.erb index a4a5b5575..6f4f13af7 100644 --- a/app/views/practices/search_partials/_category_checkbox.html.erb +++ b/app/views/practices/search_partials/_category_checkbox.html.erb @@ -11,6 +11,7 @@ type="checkbox" name="<%= category_name %>" value="<%= category_name %>" + data-parent-category="<%= parent_category_name %>" >
+ -
- - -
\ No newline at end of file + diff --git a/app/views/practices/search_partials/_visn_facility_combo_box.html.erb b/app/views/practices/search_partials/_visn_facility_combo_box.html.erb index 4334348c1..b5f807a6d 100644 --- a/app/views/practices/search_partials/_visn_facility_combo_box.html.erb +++ b/app/views/practices/search_partials/_visn_facility_combo_box.html.erb @@ -1,11 +1,12 @@ -
-
diff --git a/app/views/practices/show/search_terms/_category.html.erb b/app/views/practices/show/search_terms/_category.html.erb index 022a75a9f..1509000e8 100644 --- a/app/views/practices/show/search_terms/_category.html.erb +++ b/app/views/practices/show/search_terms/_category.html.erb @@ -1,5 +1,5 @@ + href="/search?category=<%= term %>"> <%= term %> diff --git a/app/views/shared/_search_sort_select_section.html.erb b/app/views/shared/_search_sort_select_section.html.erb index f972dbdd6..39a8d39db 100644 --- a/app/views/shared/_search_sort_select_section.html.erb +++ b/app/views/shared/_search_sort_select_section.html.erb @@ -1,7 +1,7 @@ <% label_text = local_assigns[:is_crh_show] ? 'Sort by' : '' %>
-

+

<%= label_tag 'search_sort_option', label_text, class: 'usa-label margin-0', 'aria-label': 'Search sort options'%> <%= select_tag('', options_for_select(sort_options), id: 'search_sort_option', name: 'search_sort_option', class: 'height-5 usa-select margin-0') %> diff --git a/app/views/visns/_show.js.erb b/app/views/visns/_show.js.erb index 1fae99f0d..094b178f2 100644 --- a/app/views/visns/_show.js.erb +++ b/app/views/visns/_show.js.erb @@ -64,7 +64,7 @@ function searchPracticesByVisn() { $(SEARCH_RESULTS).removeClass('display-none'); // Print results to the page document.querySelector('#search-results').innerHTML = finalResults; - + $(SEARCH_RESULTS).showMoreItems({ startNum: 6, afterNum: 6, @@ -78,7 +78,7 @@ function searchPracticesByVisn() { // Trim whitespace from the query (can cause matching problems) query = query.trim(); // Set variables - var resultsSummary = document.querySelector('#results-summary'); + var resultsSummary = document.querySelector('#results-sort'); // set the search page search bar input value searchField.value = query; if (query !== '') { diff --git a/spec/features/about_spec.rb b/spec/features/about_spec.rb index cfd0d19b4..85d357229 100644 --- a/spec/features/about_spec.rb +++ b/spec/features/about_spec.rb @@ -26,7 +26,7 @@ all('.usa-accordion__heading')[2].click click_link('Emerging') expect(page).to have_content('Search') - expect(page).to have_content('2 results:') + expect(page).to have_content('2 Results:') expect(page).to have_content('Project HAPPEN') expect(page).to have_content('Best Practice Ever') end diff --git a/spec/features/admin/admin_categories_spec.rb b/spec/features/admin/admin_categories_spec.rb index 4ce33bea7..b02dd47c1 100644 --- a/spec/features/admin/admin_categories_spec.rb +++ b/spec/features/admin/admin_categories_spec.rb @@ -59,7 +59,6 @@ click_button('Update Tag') expect(cache_keys).not_to include("categories") add_categories_to_cache - find('.search-filters-accordion-button').click expect(page).to have_content('Completely updated category') end @@ -72,7 +71,6 @@ click_button('Create Tag') expect(cache_keys).not_to include("categories") add_categories_to_cache - find('.search-filters-accordion-button').click expect(page).to have_content('Newest Category') end end diff --git a/spec/features/homepage_spec.rb b/spec/features/homepage_spec.rb index 6d326d65d..2db429237 100644 --- a/spec/features/homepage_spec.rb +++ b/spec/features/homepage_spec.rb @@ -49,7 +49,7 @@ fill_in('dm-homepage-search-field', with: 'James A. Haley') find('#dm-homepage-search-button').click - expect(page).to have_content('1 result:') + expect(page).to have_content('1 Result:') expect(page).to have_content(@practice.name) end end @@ -114,7 +114,7 @@ end end - describe 'search dropdown functionality', js: true do + describe 'search dropdown functionality' do before do find('#dm-homepage-search-field').click end @@ -160,7 +160,7 @@ end end - describe 'links to the search page' do + describe 'links to the search page', js: true do it 'provides a link to the search page' do within '#search-dropdown' do expect(page).to have_link('Browse all Innovations', href: '/search') @@ -169,24 +169,18 @@ it 'provides a link that navs to the search page with the filters accordian engaged' do within '#search-dropdown' do - expect(page).to have_link('Browse all Tags', href: '/search?filters=open') + expect(page).to have_link('Browse all Tags', href: '/search') click_link 'Browse all Tags' end - expect(page).to have_current_path('/search?filters=open') - expect(page).to have_css('.usa-accordion__button.search-filters-accordion-button[aria-expanded="true"]') - expect(page).to have_css('#search_filters_dropdown:not([hidden])') + expect(page).to have_current_path('/search') end - it 'provides a link that navs to the search page with all community tags engaged', js: true do + it 'provides a link that navs to the search page with all community tags engaged' do within '#search-dropdown' do expect(page).to have_link('Browse all Community Innovations', href: '/search?all_communities=true') click_link 'Browse all Community Innovations' end - - expect(page).to have_current_path('/search?all_communities=true') - filter_button = find('button.search-filters-accordion-button') - expect(filter_button).to have_text('Filters (1)') - expect(page).to have_content("1 result:") + expect(page).to have_content("1 Result:") expect(page).to have_content(@practice_3.name) end end @@ -195,6 +189,7 @@ page.send_keys :down, :down, :down, :down, :down # navigate to first category page.send_keys :enter # select category expect(page).to have_current_path('/search?category=COVID') + expect(page).to have_content("2 Results: TAG: COVID X") end it 'encodes categories & innovations with ampersands' do @@ -204,83 +199,57 @@ end it 'tracks clicks on practice links' do - expect { - find('a', text: 'The Best Practice Ever!').click - wait_for_ajax - }.to change(Ahoy::Event, :count).by(1) + event = wait_for_ahoy_js('The Best Practice Ever!') - event = Ahoy::Event.last expect(event.name).to eq("Dropdown Practice Link Clicked") expect(event.properties["practice_name"]).to eq("The Best Practice Ever!") expect(event.properties["from_homepage"]).to be_truthy end it 'tracks clicks on category links' do - expect { - find('a', text: 'COVID').click - wait_for_ajax - }.to change(Ahoy::Event, :count).by(1) + event = wait_for_ahoy_js('COVID') - event = Ahoy::Event.last expect(event.name).to eq("Category selected") expect(event.properties["category_name"]).to eq("COVID") expect(event.properties["from_homepage"]).to be_truthy end it 'tracks clicks on community links' do - expect { - find('a', text: 'VA Immersive').click - wait_for_ajax - }.to change(Ahoy::Event, :count).by(1) + event = wait_for_ahoy_js('VA Immersive') - event = Ahoy::Event.last expect(event.name).to eq("Category selected") expect(event.properties["category_name"]).to eq("VA Immersive") expect(event.properties["from_homepage"]).to be_truthy end it 'tracks clicks on "Browse all Innovations" link' do - expect { - find('a', text: 'Browse all Innovations').click - wait_for_ajax - }.to change(Ahoy::Event, :count).by(1) - - event = Ahoy::Event.last + event = wait_for_ahoy_js('Browse all Innovations') expect(event.name).to eq("Dropdown Browse-all Link Clicked") expect(event.properties["type"]).to eq("innovation") end it 'tracks clicks on "Browse all Tags" link' do - expect { - find('a', text: 'Browse all Tags').click - wait_for_ajax - }.to change(Ahoy::Event, :count).by(1) + event = wait_for_ahoy_js('Browse all Tags') - event = Ahoy::Event.last expect(event.name).to eq("Dropdown Browse-all Link Clicked") expect(event.properties["type"]).to eq("category") end it 'tracks clicks on "Browse all Community Innovations" link' do - expect { - find('a', text: 'Browse all Community Innovations').click - wait_for_ajax - }.to change(Ahoy::Event, :count).by(1) + event = wait_for_ahoy_js('Browse all Community Innovations') - event = Ahoy::Event.last expect(event.name).to eq("Dropdown Browse-all Link Clicked") expect(event.properties["type"]).to eq("community") end end - def wait_for_ajax - Timeout.timeout(Capybara.default_max_wait_time) do - loop until finished_all_ajax_requests? - end - end - - def finished_all_ajax_requests? - page.evaluate_script('jQuery.active').zero? + def wait_for_ahoy_js(link_text) + find('a', text: link_text).click + # wait for js ahoy library to complete db call + sleep 1 + ahoy_events = Ahoy::Event.all + expect(ahoy_events.count).to eq(1) + ahoy_events[0] end end diff --git a/spec/features/practice_viewer/introduction_spec.rb b/spec/features/practice_viewer/introduction_spec.rb index cecdfa5f6..e832e404b 100644 --- a/spec/features/practice_viewer/introduction_spec.rb +++ b/spec/features/practice_viewer/introduction_spec.rb @@ -192,9 +192,9 @@ it 'should take the user to the search page with results that match the category that was clicked on' do all('.usa-tag').first.click - expect(page).to have_current_path('/search?filter_by=COVID') + expect(page).to have_current_path('/search?category=COVID') expect(page).to have_selector('#search-page', visible: true) - expect(page).to have_content('2 results') + expect(page).to have_content('2 Results') expect(page).to have_content('A public maximum practice') expect(page).to have_content('Another public maximum practice') end @@ -215,7 +215,7 @@ it 'should take the user to the search results page when the See more practices link is clicked' do click_link('See more emerging innovations') expect(page).to have_selector('#search-page', visible: true) - expect(page).to have_content('1 result') + expect(page).to have_content('1 Result') expect(page).to have_content('A public maximum practice') end end diff --git a/spec/features/search_spec.rb b/spec/features/search_spec.rb index 3419cc09a..9609d200d 100644 --- a/spec/features/search_spec.rb +++ b/spec/features/search_spec.rb @@ -7,6 +7,7 @@ def user_login end before do + Rails.cache.clear visn_1 = Visn.find_or_create_by!(name: "VA New England Healthcare System", number: 1) visn_2 = Visn.find_or_create_by!(name: "New York/New Jersey VA Health Care Network", number: 2) Visn.find_or_create_by!(name: "VA Healthcare - VISN 4", number: 4) @@ -115,12 +116,8 @@ def search find('#dm-practice-search-button').click end - def toggle_filters_accordion - find('.search-filters-accordion-button').click - end - def update_results - click_button('Update results') + click_button('Apply filters') end def update_practice_introduction(practice) @@ -156,7 +153,9 @@ def set_combobox_val(index, value) end def select_category(label_class) - find(label_class).click + within('#search-filters') do + find(label_class).click + end end def add_crh_adoptions_and_practice_origin_facilities @@ -212,7 +211,6 @@ def add_crh_adoptions_and_practice_origin_facilities it 'should display certain text if no matches are found' do visit_search_page - toggle_filters_accordion fill_in('dm-practice-search-field', with: 'test') set_combobox_val(0, 'James J. Howard Veterans\' Outpatient Clinic') select_category('.cat-2-label') @@ -234,7 +232,7 @@ def add_crh_adoptions_and_practice_origin_facilities fill_in('dm-practice-search-field', with: 'practice') find('#dm-practice-search-button').click - expect(page).to have_content('13 results') + expect(page).to have_content('13 Results') expect(page).to_not have_content(@practice2.name) # show practices that are approved/published @@ -244,7 +242,7 @@ def add_crh_adoptions_and_practice_origin_facilities search expect(page).to be_accessible.according_to :wcag2a, :section508 - expect(page).to have_content('14 results') + expect(page).to have_content('14 Results') expect(page).to have_content(@practice2.name) end @@ -255,7 +253,7 @@ def add_crh_adoptions_and_practice_origin_facilities find('#dm-practice-search-button').click expect(page).to have_content(@practice.name) - expect(page).to have_content('1 result') + expect(page).to have_content('1 Result') end it 'should be able to search based on practice categories related terms' do @@ -266,7 +264,7 @@ def add_crh_adoptions_and_practice_origin_facilities fill_in('dm-practice-search-field', with: 'Coronavirus') find('#dm-practice-search-button').click - expect(page).to have_content('5 results') + expect(page).to have_content('5 Results') expect(page).to have_content(@practice.name) expect(page).to have_content(@practice3.name) expect(page).to have_content(@practice5.name) @@ -283,7 +281,7 @@ def add_crh_adoptions_and_practice_origin_facilities find('#dm-practice-search-button').click expect(page).to have_content(@practice.name) - expect(page).to have_content('1 result') + expect(page).to have_content('1 Result') end it 'should be able to search based on originating facility name' do @@ -292,7 +290,7 @@ def add_crh_adoptions_and_practice_origin_facilities fill_in('dm-practice-search-field', with: 'Togus VA Medical Center') find('#dm-practice-search-button').click - expect(page).to have_content('2 results') + expect(page).to have_content('2 Results') expect(page).to have_content(@practice3.name) expect(page).to have_content(@practice5.name) end @@ -301,19 +299,19 @@ def add_crh_adoptions_and_practice_origin_facilities visit_search_page fill_in('dm-practice-search-field', with: 'overview problem foobar') find('#dm-practice-search-button').click - expect(page).to have_content('1 result') + expect(page).to have_content('1 Result') expect(page).to have_content(@practice.name) visit_search_page fill_in('dm-practice-search-field', with: 'overview solution fizzbuzz') find('#dm-practice-search-button').click - expect(page).to have_content('1 result') + expect(page).to have_content('1 Result') expect(page).to have_content(@practice3.name) visit_search_page fill_in('dm-practice-search-field', with: 'overview results mixmax') find('#dm-practice-search-button').click - expect(page).to have_content('1 result') + expect(page).to have_content('1 Result') expect(page).to have_content(@practice5.name) end @@ -324,12 +322,12 @@ def add_crh_adoptions_and_practice_origin_facilities fill_in('dm-practice-search-field', with: 'important') search - expect(page).to have_content('1 result') + expect(page).to have_content('1 Result') expect(page).to have_content('The Most Important Practice') fill_in('dm-practice-search-field', with: 'Rule') search - expect(page).to_not have_content('results') + expect(page).to have_content('0 Results') expect(page).to_not have_content('One Practice to Rule Them All') expect(page).to have_content('There are currently no matches for your search on the Marketplace.') @@ -345,7 +343,7 @@ def add_crh_adoptions_and_practice_origin_facilities fill_in('dm-practice-search-field', with: 'Rule') search - expect(page).to have_content('1 result') + expect(page).to have_content('1 Result') expect(page).to have_content('One Practice to Rule Them All') expect(page).to_not have_content('There are currently no matches for your search on the Marketplace.') end @@ -354,14 +352,13 @@ def add_crh_adoptions_and_practice_origin_facilities it 'should collect practices that match ANY of the conditions if the user selects filters, but does not use the search input' do visit_search_page - toggle_filters_accordion set_combobox_val(0, 'Norwood VA Clinic') select_category('.cat-2-label') select_category('.cat-5-label') update_results - expect(page).to have_content('Filters (3)') - expect(page).to have_content('6 results') + expect(page).to have_content('6 Results') + expect(page).to have_button('Clear filters') expect(page).to have_content(@practice.name) expect(page).to have_content(@practice3.name) expect(page).to have_content(@practice5.name) @@ -374,28 +371,24 @@ def add_crh_adoptions_and_practice_origin_facilities # reset the practice cache visit_search_page - toggle_filters_accordion - click_button('Reset filters') set_combobox_val(0, 'VISN 8 Clinical Resource Hub (Remote)') update_results - expect(page).to have_content('2 results') + expect(page).to have_content('2 Results') expect(page).to have_content(@practice13.name) expect(page).to have_content(@practice14.name) - toggle_filters_accordion - click_button('Reset filters') + click_button('Clear filters') set_combobox_val(1, 'VISN 8 Clinical Resource Hub (Remote)') update_results - expect(page).to have_content('2 results') + expect(page).to have_content('2 Results') expect(page).to have_content(@practice12.name) expect(page).to have_content(@practice14.name) end it 'should select all subcategories when selecting the parent category' do visit_search_page - toggle_filters_accordion select_category('.cat-all-strategic-label') expect(find("#cat-2-input", visible: false)).to be_checked expect(find("#cat-3-input", visible: false)).to be_checked @@ -403,8 +396,7 @@ def add_crh_adoptions_and_practice_origin_facilities expect(find("#cat-5-input", visible: false)).to be_checked expect(find("#cat-6-input", visible: false)).to be_checked update_results - expect(page).to have_content('7 results') - toggle_filters_accordion + expect(page).to have_content('7 Results') select_category('.cat-2-label') expect(find("#cat-all-strategic-input", visible: false)).to_not be_checked expect(find("#cat-2-input", visible: false)).to_not be_checked @@ -413,7 +405,7 @@ def add_crh_adoptions_and_practice_origin_facilities expect(find("#cat-5-input", visible: false)).to be_checked expect(find("#cat-6-input", visible: false)).to be_checked update_results - expect(page).to have_content('5 results') + expect(page).to have_content('5 Results') end it 'should collect practices that match ALL of the conditions if the user selects filters AND uses the search input' do @@ -421,55 +413,45 @@ def add_crh_adoptions_and_practice_origin_facilities visit_search_page fill_in('dm-practice-search-field', with: 'practice') - toggle_filters_accordion set_combobox_val(0, 'Togus VA Medical Center') select_category('.cat-2-label') search - expect(page).to have_content('Filters (2)') - expect(page).to have_content('2 results') + expect(page).to have_content('2 Results') expect(page).to have_content(@practice3.name) expect(page).to have_content(@practice5.name) # Now add the remaining to eliminate one of the last two practices - toggle_filters_accordion set_combobox_val(1, 'Aberdeen VA Clinic') update_results - expect(page).to have_content('Filters (3)') - expect(page).to have_content('1 result') + expect(page).to have_content('1 Result') expect(page).to have_content(@practice3.name) expect(page).to_not have_content(@practice5.name) # Reset filters and select a VISN from the Originating Facility combo box - toggle_filters_accordion - click_button('Reset filters') + click_button('Clear filters') set_combobox_val(0, 'VISN-1') update_results - expect(page).to have_content('Filters (1)') - expect(page).to have_content('4 results') + expect(page).to have_content('4 Results') expect(page).to have_content(@practice3.name) expect(page).to have_content(@practice4.name) expect(page).to have_content(@practice5.name) expect(page).to have_content(@practice6.name) - toggle_filters_accordion select_category('.cat-4-label') update_results - expect(page).to have_content('Filters (2)') - expect(page).to have_content('2 results') + expect(page).to have_content('2 Results') expect(page).to have_content(@practice4.name) expect(page).to have_content(@practice5.name) # Reset filters and select a VISN from the Adopting Facility combo box - toggle_filters_accordion - click_button('Reset filters') + click_button('Clear filters') set_combobox_val(1, 'VISN-7') update_results - expect(page).to have_content('Filters (1)') - expect(page).to have_content('1 result') + expect(page).to have_content('1 Result') expect(page).to have_content(@practice6.name) # search for practices with the search bar and clinical resource hub filters @@ -477,13 +459,11 @@ def add_crh_adoptions_and_practice_origin_facilities visit_search_page fill_in('dm-practice-search-field', with: 'important') - toggle_filters_accordion set_combobox_val(0, 'VISN 8 Clinical Resource Hub (Remote)') set_combobox_val(1, 'VISN 8 Clinical Resource Hub (Remote)') update_results - expect(page).to have_content('Filters (2)') - expect(page).to have_content('1 result') + expect(page).to have_content('1 Result') expect(page).to have_content(@practice14.name) end @@ -491,12 +471,10 @@ def add_crh_adoptions_and_practice_origin_facilities it 'should, when the user selects a VISN, collect practices that either have that VISN as their initiating_facility OR have a practice_origin_facility that belongs to that VISN' do visit_search_page - toggle_filters_accordion set_combobox_val(0, 'VISN-1') update_results - expect(page).to have_content('Filters (1)') - expect(page).to have_content('4 results') + expect(page).to have_content('4 Results') expect(page).to have_content(@practice3.name) expect(page).to have_content(@practice3.name) expect(page).to have_content(@practice5.name) @@ -506,12 +484,10 @@ def add_crh_adoptions_and_practice_origin_facilities add_crh_adoptions_and_practice_origin_facilities visit_search_page - toggle_filters_accordion set_combobox_val(0, 'VISN-8') update_results - expect(page).to have_content('Filters (1)') - expect(page).to have_content('2 results') + expect(page).to have_content('2 Results') expect(page).to have_content(@practice13.name) expect(page).to have_content(@practice14.name) end @@ -519,11 +495,9 @@ def add_crh_adoptions_and_practice_origin_facilities it 'should, when the user selects a facility, only collect practices that have a practice_origin_facility that matches the selected facility' do visit_search_page - toggle_filters_accordion set_combobox_val(0, 'Vinita VA Clinic') update_results - expect(page).to have_content('1 result') expect(page).to have_content(@practice12.name) end end @@ -532,11 +506,10 @@ def add_crh_adoptions_and_practice_origin_facilities it 'should, when the user selects a VISN, collect practices that have an adoption facility that belongs to that VISN' do visit_search_page - toggle_filters_accordion set_combobox_val(1, 'VISN-23') update_results - expect(page).to have_content('2 results') + expect(page).to have_content('2 Results') expect(page).to have_content(@practice.name) expect(page).to have_content(@practice3.name) @@ -544,12 +517,10 @@ def add_crh_adoptions_and_practice_origin_facilities add_crh_adoptions_and_practice_origin_facilities visit_search_page - toggle_filters_accordion set_combobox_val(1, 'VISN-8') update_results - expect(page).to have_content('Filters (1)') - expect(page).to have_content('2 results') + expect(page).to have_content('2 Results') expect(page).to have_content(@practice12.name) expect(page).to have_content(@practice14.name) end @@ -557,61 +528,125 @@ def add_crh_adoptions_and_practice_origin_facilities it 'should, when the user selects a facility, only collect practices that have an adopting facility that matches the selected facility' do visit_search_page - toggle_filters_accordion set_combobox_val(0, 'Vinita VA Clinic') update_results - expect(page).to have_content('1 result') + expect(page).to have_content('1 Result') expect(page).to have_content(@practice12.name) end end + + describe 'Dynamic applied filters', js: true do + before do + visit_search_page + + set_combobox_val(0, 'Norwood VA Clinic') + set_combobox_val(1, 'Marietta VA Clinic') + select_category('.cat-2-label') + select_category('.cat-5-label') + update_results + end + + it "should display applied filters with 'Clear filters' button" do + expect(page).to have_content('TAG: COVID') + expect(page).to have_content('TAG: PULMONARY CARE') + expect(page).to have_content('ORIGIN: NORWOOD VA CLINIC') + expect(page).to have_content('ADOPTION: MARIETTA VA CLINIC') + expect(page).to have_button('Clear filters') + end + + it 'should allow filters to be individually removed to update results' do + expect(page).to have_content('6 Results') + within('#searchResultsContainer') do + all('button.applied-filter').first.click + end + + # expect updated applied filters + expect(page).not_to have_content('TAG: COVID') + expect(page).to have_content('TAG: PULMONARY CARE') + expect(page).to have_content('ORIGIN: NORWOOD VA CLINIC') + expect(page).to have_content('ADOPTION: MARIETTA VA CLINIC') + expect(page).to have_button('Clear filters') + # expect updated results + expect(page).to have_content('1 Result') + expect(page).not_to have_content(@practice.name) + expect(page).not_to have_content(@practice3.name) + expect(page).not_to have_content(@practice5.name) + expect(page).to have_content(@practice7.name) + expect(page).not_to have_content(@practice12.name) + # expect updated filter checkboxes + label = find('.cat-2-label') + parent_div = label.find(:xpath, './..') + checkbox = parent_div.find('input[type="checkbox"]', visible: :all) + expect(checkbox).not_to be_checked + label = find('.cat-5-label') + parent_div = label.find(:xpath, './..') + checkbox = parent_div.find('input[type="checkbox"]', visible: :all) + expect(checkbox).to be_checked + end + + it "should remove the 'Clear filters' button when all applied filters are removed" do + find_button('COVID').click + find_button('Pulmonary Care').click + find_button('Norwood VA Clinic').click + find_button('Marietta VA Clinic').click + + expect(page).not_to have_button('Clear filters') + end + + it 'should clear all filters' do + click_button('Clear filters') + + expect(page).not_to have_content('TAG: COVID') + expect(page).not_to have_content('TAG: PULMONARY CARE') + expect(page).not_to have_content('ORIGIN: NORWOOD VA CLINIC') + expect(page).not_to have_content('ADOPTION: MARIETTA VA CLINIC') + expect(page).not_to have_button('Clear filters') + end + end end describe 'Sorting' do it 'should sort the results based on the sort option chosen' do visit_search_page - - toggle_filters_accordion set_combobox_val(0, 'VISN-1') select_category('.cat-2-label') update_results # results should be sorted my most relevant(closest match) by default - expect(page).to have_content('6 results') - expect(first('span.dm-practice-title').text).to eq(@practice6.name) - - toggle_filters_accordion + expect(page).to have_content('6 Results') + expect(first('a.dm-link-title').text).to eq(@practice6.name) select_category('.cat-3-label') select_category('.cat-4-label') update_results - expect(page).to have_content('6 results') - expect(first('span.dm-practice-title').text).to_not eq(@practice6.name) - expect(first('span.dm-practice-title').text).to eq(@practice4.name) + expect(page).to have_content('6 Results') + expect(first('a.dm-link-title').text).to_not eq(@practice6.name) + expect(first('a.dm-link-title').text).to eq(@practice4.name) # choose 'A to Z' option - select('Sort by A to Z', from: 'search_sort_option') - expect(all('span.dm-practice-title').first.text).to eq(@practice4.name) - expect(all('span.dm-practice-title')[1].text).to eq(@practice6.name) - expect(all('span.dm-practice-title')[2].text).to eq(@practice5.name) - expect(all('span.dm-practice-title')[3].text).to eq(@practice3.name) - expect(all('span.dm-practice-title')[4].text).to eq(@practice.name) - expect(all('span.dm-practice-title')[5].text).to eq(@practice12.name) + find('label', text: 'Sort by A to Z').click + expect(all('a.dm-link-title').first.text).to eq(@practice4.name) + expect(all('a.dm-link-title')[1].text).to eq(@practice6.name) + expect(all('a.dm-link-title')[2].text).to eq(@practice5.name) + expect(all('a.dm-link-title')[3].text).to eq(@practice3.name) + expect(all('a.dm-link-title')[4].text).to eq(@practice.name) + expect(all('a.dm-link-title')[5].text).to eq(@practice12.name) # choose 'most adoptions' option - select('Sort by most adopted innovations', from: 'search_sort_option') - expect(all('span.dm-practice-title').first.text).to eq(@practice.name) - expect(all('span.dm-practice-title')[1].text).to eq(@practice3.name) - expect(all('span.dm-practice-title')[2].text).to eq(@practice6.name) + find('label', text: 'Sort by most adopted innovations').click + expect(all('a.dm-link-title').first.text).to eq(@practice.name) + expect(all('a.dm-link-title')[1].text).to eq(@practice3.name) + expect(all('a.dm-link-title')[2].text).to eq(@practice6.name) # choose 'most recently added' option - select('Sort by most recently added', from: 'search_sort_option') - expect(all('span.dm-practice-title').first.text).to eq(@practice12.name) - expect(all('span.dm-practice-title')[1].text).to eq(@practice6.name) - expect(all('span.dm-practice-title')[2].text).to eq(@practice5.name) - expect(all('span.dm-practice-title')[3].text).to eq(@practice4.name) - expect(all('span.dm-practice-title')[4].text).to eq(@practice3.name) - expect(all('span.dm-practice-title')[5].text).to eq(@practice.name) + find('label', text: 'Sort by most recently added').click + expect(all('a.dm-link-title').first.text).to eq(@practice12.name) + expect(all('a.dm-link-title')[1].text).to eq(@practice6.name) + expect(all('a.dm-link-title')[2].text).to eq(@practice5.name) + expect(all('a.dm-link-title')[3].text).to eq(@practice4.name) + expect(all('a.dm-link-title')[4].text).to eq(@practice3.name) + expect(all('a.dm-link-title')[5].text).to eq(@practice.name) end end @@ -622,20 +657,18 @@ def add_crh_adoptions_and_practice_origin_facilities fill_in('dm-practice-search-field', with: 'practice') search - expect(page).to have_content('13 results') - expect(page).to have_selector('div.dm-practice-card', count: 12) + expect(page).to have_content('13 Results') + expect(page).to have_selector('div.dm-search-result', count: 12) # show the next set of 12 results click_button('Load more') - expect(page).to have_selector('div.dm-practice-card', count: 13) + expect(page).to have_selector('div.dm-search-result', count: 13) end end describe 'Querying' do it 'should not create a query if the user does not enter text into the search input' do visit_search_page - - toggle_filters_accordion set_combobox_val(0, 'VISN-2') select_category('.cat-2-label') select_category('.cat-3-label') @@ -656,11 +689,9 @@ def add_crh_adoptions_and_practice_origin_facilities it 'should, if there is no query and the user clicks on a practice card, add a search page breadcrumb that does not include a query' do visit_search_page - - toggle_filters_accordion select_category('.cat-2-label') update_results - all('.dm-practice-link-aria-hidden').first.click + all('.dm-link-title').first.click expect(page).to have_link('Search', href: '/search') end @@ -670,7 +701,7 @@ def add_crh_adoptions_and_practice_origin_facilities fill_in('dm-practice-search-field', with: 'test') search - all('.dm-practice-link-aria-hidden').first.click + all('.dm-link-title').first.click expect(page).to have_link('Search', href: '/search?query=test') end @@ -689,7 +720,7 @@ def add_crh_adoptions_and_practice_origin_facilities end end - describe 'Cache' do + describe 'Cache', js: true do it 'Should be reset if certain practice attributes have been updated' do add_search_to_cache expect(cache_keys).to include("searchable_practices_json") @@ -703,9 +734,7 @@ def add_crh_adoptions_and_practice_origin_facilities add_search_to_cache expect(cache_keys).to include("searchable_practices_json") login_as(@admin, :scope => :user, :run_callbacks => false) - visit '/admin' - click_link('Practices') - click_link('New Practice') + visit '/admin/practices/new' fill_in('Innovation name', with: 'The Newest Practice') fill_in('User email', with: 'practice_owner@va.gov') click_button('Create Practice') @@ -741,16 +770,16 @@ def add_crh_adoptions_and_practice_origin_facilities it 'should function appropriately' do visit_search_page - click_button('Filters') - click_button('Originating facility') + click_button('Refine Results') + # click_button('Originating facility') set_combobox_val(0, 'Togus VA Medical Center') - click_button('Adopting facility') + # click_button('Adopting facility') set_combobox_val(1, 'VISN-2') - click_button('Tags') + # click_button('Tags') select_category('.cat-5-label') update_results - expect(page).to have_content('5 results') + expect(page).to have_content('5 Results') expect(page).to have_content(@practice.name) expect(page).to have_content(@practice3.name) expect(page).to have_content(@practice5.name) @@ -758,4 +787,122 @@ def add_crh_adoptions_and_practice_origin_facilities expect(page).to have_content(@practice10.name) end end + + describe 'search header' do + it 'should display appropriate header text when page loads' do + visit_search_page + expect(find('#searchHeader')).to have_text('Search') + expect(find('#searchHeader')).not_to have_text('Search Results for:') + + visit '/' + find('#dm-homepage-search-field').click + fill_in('dm-homepage-search-field', with: 'test') + find_button('dm-homepage-search-button').click + + expect(find('#searchHeader')).to have_text('Search Results for: test') + end + + it 'should update the search header when a query is entered and removed in search bar' do + visit_search_page + + fill_in('dm-practice-search-field', with: 'test') + search + expect(find('#searchHeader')).to have_text('Search Results for: test') + + fill_in('dm-practice-search-field', with: '') + search + expect(find('#searchHeader')).to have_text('Search') + expect(find('#searchHeader')).not_to have_text('Search Results for:') + end + end + + describe 'Keyboard Accessibility', js: true do + before do + visit_search_page + end + + it 'should allow navigation through interactive elements using the Tab key' do + find('body').send_keys(:tab) until page.evaluate_script('document.activeElement.id') == 'dm-practice-search-field' + + find('body').send_keys(:tab) + expect(page.evaluate_script('document.activeElement.id')).to eq('dm-practice-search-button') + + find('body').send_keys(:tab) + expect(page.evaluate_script('document.activeElement.id')).to eq('search_sort_option_adoptions') + + 3.times { find('body').send_keys(:tab) } + expect(page.evaluate_script('document.activeElement.className')).to include('usa-checkbox__input') + + find('body').send_keys(:tab) until page.evaluate_script('document.activeElement.id') == 'update-search-results-button' + expect(page.evaluate_script('document.activeElement.id')).to eq('update-search-results-button') + + find('body').send_keys(:tab) until page.evaluate_script('document.activeElement.className').include?('dm-link-title') + expect(page.evaluate_script('document.activeElement.className')).to include('dm-link-title') + + find('body').send_keys(:tab) + expect(page.evaluate_script('document.activeElement.className')).to include('dm-link-title') + end + + it 'should allow changing sort to A to Z using the keyboard and verify the top result' do + expect(first('a.dm-link-title').text).to eq(@practice.name) + find('body').send_keys(:tab) until page.evaluate_script('document.activeElement.id') == 'search_sort_option_adoptions' + find('body').send_keys(:arrow_up) + expect(page.evaluate_script('document.activeElement.id')).to eq('search_sort_option_a_to_z') + expect(first('a.dm-link-title').text).to eq(@practice4.name) + end + + context 'filters' do + it 'should allow adding and removing of single filter' do + find('body').send_keys(:tab) until page.evaluate_script('document.activeElement.id') == "cat-#{@cat_1.id}-input" + find('body').send_keys(:space) + expect(page.evaluate_script('document.activeElement.checked')).to be true + + find('body').send_keys(:tab) until page.evaluate_script('document.activeElement.id') == 'update-search-results-button' + find('body').send_keys(:enter) + expect(page).to have_content(@practice.name) + expect(page).to have_content(@practice3.name) + expect(page).not_to have_content(@practice4.name) + + find('body').send_keys(:shift, :tab) until page.evaluate_script('document.activeElement.textContent').include?('TAG: COVID') + find('body').send_keys(:space) + expect(page).to have_content(@practice.name) + expect(page).to have_content(@practice3.name) + expect(page).to have_content(@practice4.name) + end + + it 'should allow adding and removing of all filters' do + find('body').send_keys(:tab) until page.evaluate_script('document.activeElement.id') == "cat-#{@cat_1.id}-input" + find('body').send_keys(:space) + find('body').send_keys(:tab) + find('body').send_keys(:space) + find('body').send_keys(:tab) until page.evaluate_script('document.activeElement.id') == 'update-search-results-button' + find('body').send_keys(:enter) + expect(page).to have_content("TAG: COVID X") + expect(page).to have_content("TAG: ENVIRONMENTAL SERVICES X") + find('body').send_keys(:shift, :tab) until page.evaluate_script('document.activeElement.id').include?('reset-search-filters-button') + find('body').send_keys(:enter) + expect(page).not_to have_content("TAG: COVID X") + expect(page).not_to have_content("TAG: ENVIRONMENTAL SERVICES X") + end + end + + it 'should allow selecting adopting and originating locations' do + find('body').send_keys(:tab) until page.evaluate_script('document.activeElement.id') == 'originating-facility-and-visn-select' + find('body').send_keys(:space) + 3.times { find('body').send_keys(:arrow_down) } + find('body').send_keys(:enter) + + find('body').send_keys(:tab) until page.evaluate_script('document.activeElement.id') == 'adopting-facility-and-visn-select' + find('body').send_keys(:space) + 8.times { find('body').send_keys(:arrow_down) } + find('body').send_keys(:enter) + + find('body').send_keys(:tab) until page.evaluate_script('document.activeElement.id') == 'update-search-results-button' + find('body').send_keys(:space) + expect(page).to have_content('ADOPTION: EAST LIVERPOOL VA CLINIC X') + expect(page).to have_content('ORIGIN: NEWARK VA CLINIC X') + expect(page).to have_content('1 Result') + expect(page).to have_content('The Most Important Practice') + end + end end diff --git a/spec/features/shared/breadcrumbs_spec.rb b/spec/features/shared/breadcrumbs_spec.rb index e48ced83b..5e18d3072 100644 --- a/spec/features/shared/breadcrumbs_spec.rb +++ b/spec/features/shared/breadcrumbs_spec.rb @@ -125,7 +125,7 @@ find('#dm-navbar-search-desktop-button').click expect(page).to have_content('The Best') expect(page).to be_accessible.according_to :wcag2a, :section508 - find("a[href='#{practice_path(@user_practice)}'][aria-hidden='true']").click + find("a[href='#{practice_path(@user_practice)}']").click expect(page).to have_css("#pr-view-introduction", visible: true) within(:css, '#breadcrumbs') do expect(page).to have_css('.fa-arrow-left') diff --git a/spec/features/shared/header_spec.rb b/spec/features/shared/header_spec.rb index 8963a77ec..9ff3a82f8 100644 --- a/spec/features/shared/header_spec.rb +++ b/spec/features/shared/header_spec.rb @@ -177,7 +177,7 @@ find('#dm-navbar-search-desktop-button').click end - expect(page).to have_content('1 result') + expect(page).to have_content('1 Result') expect(page).to have_content('A public practice') expect(page).to have_current_path('/search?query=test') @@ -214,7 +214,7 @@ open_mobile_menu fill_in('dm-navbar-search-mobile-field', with: 'test') find('#dm-navbar-search-mobile-button').click - expect(page).to have_content('1 result') + expect(page).to have_content('1 Result') expect(page).to have_content('A public practice') expect(page).to have_current_path('/search?query=test') diff --git a/spec/features/users/recommended_for_you_spec.rb b/spec/features/users/recommended_for_you_spec.rb index 7006fbe6b..d8ddc058e 100644 --- a/spec/features/users/recommended_for_you_spec.rb +++ b/spec/features/users/recommended_for_you_spec.rb @@ -110,7 +110,7 @@ def login_and_visit_recommended_path(user) click_link('See more innovations') expect(page).to have_selector('#search-page', visible: true) - expect(page).to have_content('4 results') + expect(page).to have_content('4 Results') end end