diff --git a/css/pb.css b/css/pb.css index 7b8f865c2e8717365f631bc80576670a6ba2301b..9336c78a8c16fb2f08832e3a1a92dc978d71f5b1 100644 --- a/css/pb.css +++ b/css/pb.css @@ -5,9 +5,6 @@ margin: 0; padding: 0; } -.pb-filter__heading--narrow { - margin-block-start: 0; -} .pb-filter__summary { padding: 0; cursor: pointer; @@ -15,27 +12,57 @@ .pb-filter__summary--open { padding-bottom: 0.95rem; } -.pb-filter__heading { - display: inline; - margin-top: 0; - padding: 0 0.5rem; - font-size: 1rem; -} .pb-filter__checkbox-label { - display: block; - padding: 5px 0; + display: flex; + align-items: center; + gap: 1rem; + padding-left: 1rem; + border-bottom: 1px solid rgba(212, 212, 218, 0.6); } -.pb-filter__checkbox { - margin-inline: 10px; +.pb-filter__checkbox-label:hover, +.pb-filter__checkbox-label:focus { + background-color: #f3f4f9; } + +.pb-filter__checkbox-label-txt { + display: block; + flex-grow: 1; + padding: 1rem 0; + cursor: pointer; +} + .pb-filter__fieldset { border: none; } -.pb-filter__checkbox-label, -.pb-filter__checkbox { +.pb-filter__multi-dropdown { + min-width: 10rem; +} +.pb-filter__multi-dropdown__label { + position: relative; + display: inline-block; cursor: pointer; } +.pb-filter__multi-dropdown { + position: relative; +} + +.pb-filter__multi-dropdown__items--hidden { + display: none; +} +.pb-filter__multi-dropdown__items--visible { + position: absolute; + z-index: 100; + top: 47px; + right: 0; + display: block; + overflow: scroll; + width: 100%; + max-height: 380px; + background-color: #fff; + box-shadow: 0 0.5rem 0.5rem 0 rgba(0, 0, 0, 0.1); +} + /* <ImageCarousel> */ .pb-image-carousel { display: flex; @@ -171,7 +198,6 @@ .pb-layout__header { display: flex; justify-content: space-between; - border-bottom: 1px solid #dee2e6; } .pb-search-results { margin-bottom: 5px; @@ -243,7 +269,6 @@ .pb-layout { display: flex; flex-flow: column nowrap; - gap: 1.5rem; } @media screen and (min-width: 800px) { @@ -609,24 +634,65 @@ } /* <Search/Search> */ -.search__form-item { - margin-top: 0; +.search__form-container { + margin: -1px 0 2rem; + padding: 1.5rem; + border: 1px solid #dedfe4; + border-radius: 1px; + background-color: #fff; + box-shadow: 0 2px 0.25rem rgba(0, 0, 0, 0.1); +} + +@media screen and (min-width: 600px) { + .search__form-filters { + display: grid; + grid-template-columns: repeat(6, 1fr); + align-items: center; + gap: 1rem; + } + + .search__form-filters .search__bar-container { + grid-column: span 6; + } + + .search__form-filters .form-type--select, + .search__form-filters .filter-group__filter-options { + grid-column: span 3; + } } -.search__form { - display: inherit; - flex-wrap: wrap; - margin-top: 2.375rem; - padding: 0 0 1.5rem; + +@media screen and (min-width: 1200px) { + .search__form-filters { + grid-template-columns: repeat(8, 1fr); + } + + .search__form-filters .search__bar-container { + grid-column: span 8; + } + + .search__form-filters .form-type--select, + .search__form-filters .filter-group__filter-options { + grid-column: span 2; + } +} + +.search__form-filters .form-element--type-select { + width: 100%; +} + +.search__bar-container.form-item { + margin-block-end: 1.5rem; } .search__search-bar .search__search_term { - position: relative; - display: flex; width: 100%; - height: 50px; + padding: 0 3rem 0 1rem; + border: none; outline: none; } - +.search__filter-select { + min-width: 10rem; +} .search__search_term::-webkit-search-cancel-button, .search__search_term::-webkit-search-decoration, .search__search_term::-webkit-search-results-button, @@ -636,29 +702,35 @@ .search__search-bar { position: relative; - height: 50px; + min-width: 10rem; cursor: pointer; - text-align: center; color: #fff; border: 1px solid #919297; border-radius: 2px; + background-color: #fff; font-size: 20px; } +.search__search-bar:hover { + border-color: #000; +} + .search__search-icon { position: absolute; + z-index: 1; + right: 10px; bottom: 12px; - inset-inline-end: 30px; } .search__search-clear { position: absolute; + z-index: 1; top: 0; + right: 40px; height: 100%; cursor: pointer; border: none; background: none; - inset-inline-end: 60px; } .search__search_term::placeholder { @@ -677,15 +749,19 @@ border: 3px solid #f3f4f9; } -.search__grid-container { - display: grid; - grid-template-columns: 5fr auto auto; - grid-gap: 20px; - align-items: center; - max-width: 100%; - height: auto; - padding: 5px; - background: #f3f4f9; +.search__form-filters-container { + padding: 1rem 1.5rem; + background-color: #f3f4f9; +} + +@media screen and (min-width: 800px) { + .search__form-sort { + display: grid; + grid-template-columns: 5fr auto; + grid-gap: 1rem; + align-items: center; + padding-top: 1rem; + } } .search__filter-button { @@ -697,12 +773,6 @@ background: none; } -@media screen and (max-width: 855px) { - .search__grid-container { - display: block; - } -} - /* <Search/SearchFilters> */ .search__filters { display: flex; @@ -718,9 +788,10 @@ margin-inline-end: 1em; } -.search__filter-wrapper { +.search__filters { display: flex; align-items: center; + padding: 0.5rem 0; } .search__filter__toggle.form-element { @@ -763,7 +834,10 @@ .search__sort { z-index: 2; } - +.search__sort label { + font-size: 0.9rem; + font-weight: bold; +} .search__sort-select.form-element { border: none; background-color: #d3d4d9; diff --git a/sveltejs/public/build/bundle.js b/sveltejs/public/build/bundle.js index 45fe7eac640958ab74619e3ad8eede5e3da73758..77726f38845aab0952ba9f3b9f7640a3e39b1a8f 100644 Binary files a/sveltejs/public/build/bundle.js and b/sveltejs/public/build/bundle.js differ diff --git a/sveltejs/public/build/bundle.js.map b/sveltejs/public/build/bundle.js.map index 98f6940379942eb891ddfd46236c3d3f825a0565..b8b9abd3aeb50472487df518895306cb44580c54 100644 Binary files a/sveltejs/public/build/bundle.js.map and b/sveltejs/public/build/bundle.js.map differ diff --git a/sveltejs/src/Filter.svelte b/sveltejs/src/Filter.svelte index 9ad1d58025f66389f091513dc1dfcd0b2e79e3a3..a53a6e2b344c6b2e6202ad3d1c2d7b2de580de83 100644 --- a/sveltejs/src/Filter.svelte +++ b/sveltejs/src/Filter.svelte @@ -5,13 +5,103 @@ moduleCategoryVocabularies, activeTab, } from './stores'; - import MediaQuery from './MediaQuery.svelte'; import { normalizeOptions, shallowCompare } from './util'; const { Drupal } = window; const dispatch = createEventDispatcher(); const stateContext = getContext('state'); + let filterVisible = false; + + function showHideFilter() { + filterVisible = !filterVisible; + if (filterVisible) { + setTimeout(() => { + document + .getElementsByClassName('pb-filter__checkbox-label')[0] + .firstElementChild.focus(); + }, 50); + return; + } + document + .getElementsByClassName('pb-filter__multi-dropdown__items')[0] + .focus(); + } + + function onBlur(event) { + if ( + event.relatedTarget === null || + !document + .getElementsByClassName('pb-filter__multi-dropdown')[0] + .contains(event.relatedTarget) + ) { + filterVisible = false; + } + } + + function onKeyDown(event) { + // Space to open category filter drop-down. + if ( + event.key === ' ' && + event.target.classList.contains('pb-filter__multi-dropdown') + ) { + showHideFilter(); + event.preventDefault(); + return; + } + // Alt Up/Down opens/closes category filter drop-down. + if ( + event.altKey && + (event.key === 'ArrowDown' || event.key === 'ArrowUp') + ) { + showHideFilter(); + event.preventDefault(); + return; + } + // Down arrow on checkbox moves to next checkbox. + if ( + event.target.classList.contains('pb-filter__checkbox') && + event.key === 'ArrowDown' && + event.target.parentElement.nextElementSibling !== null + ) { + event.target.parentElement.nextElementSibling.firstElementChild.focus(); + event.preventDefault(); + return; + } + // Up arrow on checkbox moves to previous checkbox. + if ( + event.target.classList.contains('pb-filter__checkbox') && + event.key === 'ArrowUp' && + event.target.parentElement.previousElementSibling !== null + ) { + event.target.parentElement.previousElementSibling.firstElementChild.focus(); + event.preventDefault(); + return; + } + // Tab moves off filter. + if (event.key === 'Tab') { + if (event.shiftKey) { + // Shift+tab moves to search box. + document.getElementById('pb-text').focus(); + event.preventDefault(); + return; + } + // Tab without shift moves to next filter. + document.getElementsByName('securityCoverage')[0].focus(); + event.preventDefault(); + return; + } + + // Escape closes filter drop-down. + if ( + event.target.classList.contains('pb-filter__checkbox') && + event.key === 'Escape' + ) { + filterVisible = false; + document.getElementsByClassName('pb-filter__multi-dropdown')[0].focus(); + } + } + async function onSelectCategory(event) { const state = stateContext.getState(); const detail = { @@ -25,6 +115,12 @@ dispatch('selectCategory', detail); stateContext.setPage(0, 0); stateContext.setRows(detail.rows); + filterVisible = true; + if (event.target.classList.contains('pb-filter__checkbox')) { + setTimeout(() => { + event.target.focus(); + }, 50); + } } async function fetchAllCategories() { @@ -54,46 +150,57 @@ }); </script> -<MediaQuery query="(min-width: 800px)" let:matches> - <form class="pb-filter"> - <section aria-label={Drupal.t('Filter categories')}> - <details - class="pb-filter__categories" - class:pb-filter__categories--open={matches} - open={matches} +<section + aria-label={Drupal.t('Filter by category')} + class="search__form-item js-form-item form-item js-form-type-select form-type--select" +> + <fieldset class="pb-filter__fieldset"> + <label for="pb-text" class="form-item__label" + >{Drupal.t('Filter by category')}</label + > + {#await apiModuleCategory then categoryList} + <div + role="button" + tabindex="0" + class="pb-filter__multi-dropdown form-element form-element--type-select" + on:click={() => { + showHideFilter(); + }} + on:blur={onBlur} + on:keydown={onKeyDown} > - <summary - class="pb-filter__summary" - class:pb-filter__summary--open={matches} - hidden={matches} + <span class="pb-filter__multi-dropdown__label" + >{$moduleCategoryFilter.length + ? $moduleCategoryFilter + .map((category) => $moduleCategoryVocabularies[category]) + .join(', ') + : Drupal.t('Select categories')}</span + > + <div + class="pb-filter__multi-dropdown__items + pb-filter__multi-dropdown__items--{filterVisible + ? 'visible' + : 'hidden'}" > - <h2 class="pb-filter__heading pb-filter__heading--wide"> - {Drupal.t('Filter Categories')} - </h2> - </summary> - <fieldset class="pb-filter__fieldset"> - <h2 - class="pb-filter__heading pb-filter__heading--narrow" - class:visually-hidden={!matches} - > - {Drupal.t('Filter Categories')} - </h2> - {#await apiModuleCategory then categoryList} - {#each categoryList[$activeTab] as dt} - <label class="pb-filter__checkbox-label"> - <input - type="checkbox" - id={dt.id} - class="pb-filter__checkbox" - bind:group={$moduleCategoryFilter} - on:change={onSelectCategory} - value={dt.id} - />{dt.name}</label - > - {/each} - {/await} - </fieldset> - </details> - </section> - </form> -</MediaQuery> + {#each categoryList[$activeTab] as dt} + <div class="pb-filter__checkbox-label"> + <input + type="checkbox" + id={dt.id} + class="pb-filter__checkbox form-checkbox form-boolean form-boolean--type-checkbox" + bind:group={$moduleCategoryFilter} + on:change={onSelectCategory} + on:blur={onBlur} + on:keydown={onKeyDown} + value={dt.id} + /> + <label for={dt.id} class="pb-filter__checkbox-label-txt"> + {dt.name} + </label> + </div> + {/each} + </div> + </div> + {/await} + </fieldset> +</section> diff --git a/sveltejs/src/ProjectBrowser.svelte b/sveltejs/src/ProjectBrowser.svelte index ed881acb0730070eeb003a24eb89b017bca24712..8bf58aa2f575c2b59a300ba0885ea8631ec9b5e0 100644 --- a/sveltejs/src/ProjectBrowser.svelte +++ b/sveltejs/src/ProjectBrowser.svelte @@ -1,7 +1,7 @@ <script> import { onMount } from 'svelte'; import { withPrevious } from 'svelte-previous'; - import ProjectGrid, { Search, Filter } from './ProjectGrid.svelte'; + import ProjectGrid, { Search } from './ProjectGrid.svelte'; import Pagination from './Pagination.svelte'; import Project from './Project/Project.svelte'; import Tabs from './Tabs.svelte'; @@ -61,7 +61,6 @@ focusedElement.subscribe((value) => { element = value; }); - let filterComponent; let searchComponent; /** @@ -247,7 +246,8 @@ .forEach((t) => t.setAttribute('aria-selected', false)); // Set this tab as selected target.setAttribute('aria-selected', true); - filterComponent.setModuleCategoryVocabulary(); + // @TODO this needs to get ported into Search somehow: + // filterComponent.setModuleCategoryVocabulary(); $categoryCheckedTrack[$activeTab] = $moduleCategoryFilter; $moduleCategoryFilter = []; $activeTab = event.detail.pluginId; @@ -385,12 +385,6 @@ </div> </div> - <div slot="left"> - <Filter - on:selectCategory={onSelectCategory} - bind:this={filterComponent} - /> - </div> {#each rows as row, index (row)} <Project {toggleView} project={row} /> {/each} diff --git a/sveltejs/src/Search/FilterGroup.svelte b/sveltejs/src/Search/FilterGroup.svelte index 620b71b3060aa2d1d7f3c4b4f7b20d73352137fe..ea75622adc86915994f7a867aadd7ee0a5421363 100644 --- a/sveltejs/src/Search/FilterGroup.svelte +++ b/sveltejs/src/Search/FilterGroup.svelte @@ -7,30 +7,16 @@ export let filterType; </script> -<fieldset class="filter-group"> - <legend class="filter-group__title-wrapper"> - {filterTitle}: - </legend> - <div class="filter-group__filter-options-wrapper"> - <div class="filter-group__filter-options"> - {#each Object.entries(filterData) as [id, label]} - <div class="filter-group__filter-option"> - <input - type="radio" - name={filterType} - id={filterType + id} - class="filter-group__radio form-radio form-boolean form-boolean--type-radio" - bind:group={$filters[filterType]} - on:change={changeHandler} - value={id} - /> - <slot class="filter-group__label-slot" name="label" {id} {label}> - <label class="filter-group__option-label" for={filterType + id}> - {label} - </label> - </slot> - </div> - {/each} - </div> - </div> -</fieldset> +<div class="filter-group__filter-options form-item"> + <label for={filterType} class="form-item__label">{filterTitle}</label> + <select + name={filterType} + class="search__filter-select form-select form-element form-element--type-select" + bind:value={$filters[filterType]} + on:change={changeHandler} + > + {#each Object.entries(filterData) as [id, label]} + <option value={id}>{label}</option> + {/each} + </select> +</div> diff --git a/sveltejs/src/Search/Search.svelte b/sveltejs/src/Search/Search.svelte index 59639be63f521a2ed83b51d59dac072c7545ac60..9f846b1c26f7d076c6a1143a578319791438fd3a 100644 --- a/sveltejs/src/Search/Search.svelte +++ b/sveltejs/src/Search/Search.svelte @@ -1,9 +1,9 @@ <script> import { createEventDispatcher, getContext, onMount } from 'svelte'; import FilterApplied from './FilterApplied.svelte'; + import FilterGroup from './FilterGroup.svelte'; import { normalizeOptions, shallowCompare } from '../util'; - import SearchFilters from './SearchFilters.svelte'; - import SearchFilterToggle from './SearchFilterToggle.svelte'; + import { Filter } from '../ProjectGrid.svelte'; import SearchSort from './SearchSort.svelte'; import { filters, @@ -44,14 +44,13 @@ placeholder: Drupal.t('Module Name, Keyword(s), etc.'), }; - // eslint-disable-next-line prefer-const - let filtersOpen = false; let sortMatch = $sortCriteria.find((option) => option.id === $sort); if (typeof sortMatch === 'undefined') { $sort = $sortCriteria[0].id; sortMatch = $sortCriteria.find((option) => option.id === $sort); } let sortText = sortMatch.text; + let filterComponent; const updateVocabularies = (vocabulary, value) => { const normalizedValue = normalizeOptions(value); @@ -130,12 +129,6 @@ stateContext.setRows(detail.rows); } - function removeFilter(filterType) { - $filters[filterType] = ALL_VALUES_ID; - $filters = $filters; - onAdvancedFilter(); - } - function clearText() { $searchString = ''; onSearch(); @@ -162,13 +155,13 @@ }; </script> -<form class="search__form"> +<form class="search__form-container"> <div - class="search__form-item js-form-item form-item js-form-type-textfield form-type--textfield" + class="search__bar-container search__form-item js-form-item form-item js-form-type-textfield form-type--textfield" role="search" > <label for="pb-text" class="form-item__label" - >{Drupal.t('Search for modules')}</label + >{Drupal.t('Keyword search')}</label > <div class="search__search-bar"> <input @@ -214,65 +207,96 @@ /> </div> </div> - <div - class="search__grid-container js-form-item js-form-type-select form-type--select js-form-item-type form-item--type" - > - <section - class="search__filter-wrapper" - aria-label={Drupal.t('Search results')} + <div class="search__form-filters-container"> + <div class="search__form-filters"> + <Filter + on:selectCategory={onSelectCategory} + bind:this={filterComponent} + /> + <FilterGroup + filterTitle={Drupal.t('Security Advisory Coverage')} + filterData={SECURITY_OPTIONS} + filterType="securityCoverage" + changeHandler={onAdvancedFilter} + let:id + let:label + /> + <FilterGroup + filterTitle={Drupal.t('Maintenance Status')} + filterData={MAINTENANCE_OPTIONS} + filterType="maintenanceStatus" + changeHandler={onAdvancedFilter} + let:id + let:label + > + <label + slot="label" + class="search__checkbox-label" + for={`maintenanceStatus${id}`} + > + {label} + </label> + </FilterGroup> + <FilterGroup + filterTitle={Drupal.t('Development Status')} + filterData={DEVELOPMENT_OPTIONS} + filterType="developmentStatus" + changeHandler={onAdvancedFilter} + let:id + let:label + > + <label + slot="label" + class="search__checkbox-label" + for={`developmentStatus${id}`} + > + {label} + </label> + </FilterGroup> + </div> + <div + class="search__form-sort js-form-item js-form-type-select form-type--select js-form-item-type form-item--type" > - <SearchFilterToggle bind:isOpen={filtersOpen} /> - <div class="search__results-count"> - {#each ['developmentStatus', 'maintenanceStatus', 'securityCoverage'] as filterType} - {#if $filters[filterType]} + <section class="search__filters" aria-label={Drupal.t('Search results')}> + <div class="search__results-count"> + {#each $moduleCategoryFilter as category} <FilterApplied - id={$filters[filterType]} - label={$filtersVocabularies[filterType][$filters[filterType]]} - clickHandler={() => removeFilter(filterType)} + id={category} + label={$moduleCategoryVocabularies[category]} + clickHandler={() => { + $moduleCategoryFilter.splice( + $moduleCategoryFilter.indexOf(category), + 1, + ); + $moduleCategoryFilter = $moduleCategoryFilter; + onSelectCategory(); + }} /> - {/if} - {/each} - - {#each $moduleCategoryFilter as category} - <FilterApplied - id={category} - label={$moduleCategoryVocabularies[category]} - clickHandler={() => { - $moduleCategoryFilter.splice( - $moduleCategoryFilter.indexOf(category), - 1, - ); - $moduleCategoryFilter = $moduleCategoryFilter; - onSelectCategory(); - }} - /> - {/each} + {/each} - {#if $filters.securityCoverage !== ALL_VALUES_ID || $filters.maintenanceStatus !== ALL_VALUES_ID || $filters.developmentStatus !== ALL_VALUES_ID || $moduleCategoryFilter.length} - <button - class="search__filter-button" - type="button" - on:click|preventDefault={() => - filterResets(ALL_VALUES_ID, ALL_VALUES_ID, ALL_VALUES_ID)} - > - {Drupal.t('Clear filters')} - </button> - {/if} - {#if !($filters.maintenanceStatus === ACTIVELY_MAINTAINED_ID && $filters.securityCoverage === COVERED_ID && $filters.developmentStatus === ALL_VALUES_ID && $moduleCategoryFilter.length === 0)} - <button - class="search__filter-button" - type="button" - on:click|preventDefault={() => - filterResets(ACTIVELY_MAINTAINED_ID, ALL_VALUES_ID, COVERED_ID)} - > - {Drupal.t('Recommended filters')} - </button> - {/if} - </div> - </section> - <SearchSort on:sort bind:sortText refresh={refreshLiveRegion} /> - </div> - <div class="search__dropdown dropdown-filters" id="filter-dropdown"> - <SearchFilters bind:isOpen={filtersOpen} {onAdvancedFilter} /> + {#if $filters.securityCoverage !== ALL_VALUES_ID || $filters.maintenanceStatus !== ALL_VALUES_ID || $filters.developmentStatus !== ALL_VALUES_ID || $moduleCategoryFilter.length} + <button + class="search__filter-button" + type="button" + on:click|preventDefault={() => + filterResets(ALL_VALUES_ID, ALL_VALUES_ID, ALL_VALUES_ID)} + > + {Drupal.t('Clear filters')} + </button> + {/if} + {#if !($filters.maintenanceStatus === ACTIVELY_MAINTAINED_ID && $filters.securityCoverage === COVERED_ID && $filters.developmentStatus === ALL_VALUES_ID && $moduleCategoryFilter.length === 0)} + <button + class="search__filter-button" + type="button" + on:click|preventDefault={() => + filterResets(ACTIVELY_MAINTAINED_ID, ALL_VALUES_ID, COVERED_ID)} + > + {Drupal.t('Recommended filters')} + </button> + {/if} + </div> + </section> + <SearchSort on:sort bind:sortText refresh={refreshLiveRegion} /> + </div> </div> </form> diff --git a/sveltejs/src/Search/SearchFilterToggle.svelte b/sveltejs/src/Search/SearchFilterToggle.svelte deleted file mode 100644 index 35d33a5082500f098001933be9a6876934ab5813..0000000000000000000000000000000000000000 --- a/sveltejs/src/Search/SearchFilterToggle.svelte +++ /dev/null @@ -1,33 +0,0 @@ -<script> - import { FULL_MODULE_PATH } from '../constants'; - - const { Drupal } = window; - - // eslint-disable-next-line import/prefer-default-export - export let isOpen; - - /* When the user clicks on the button, - toggle between hiding and showing the dropdown content */ - function openDropdown() { - isOpen = !isOpen; - } -</script> - -<div class="search__filter__toggle-container"> - <section aria-label={Drupal.t('Filter settings')}> - <button - type="button" - class="search__filter__toggle form-element" - class:is_open={isOpen} - aria-controls="filter-dropdown" - aria-label={isOpen ? Drupal.t('Close Filter') : Drupal.t('Open Filter')} - aria-expanded={isOpen.toString()} - on:click={() => openDropdown()} - ><img - src="{FULL_MODULE_PATH}/images/advanced-filter-icon.svg" - alt="advanced filter icon" - class="search__filter__toggle-img" - />Filters - </button> - </section> -</div> diff --git a/sveltejs/src/Search/SearchFilters.svelte b/sveltejs/src/Search/SearchFilters.svelte deleted file mode 100644 index 3570db9cd064db295116931c5f1a6e4fd1dde7fa..0000000000000000000000000000000000000000 --- a/sveltejs/src/Search/SearchFilters.svelte +++ /dev/null @@ -1,71 +0,0 @@ -<script> - import { slide } from 'svelte/transition'; - import FilterGroup from './FilterGroup.svelte'; - import { - COVERED_ID, - MAINTENANCE_OPTIONS, - DEVELOPMENT_OPTIONS, - SECURITY_OPTIONS, - } from '../constants'; - - const { Drupal } = window; - - export let onAdvancedFilter; - export let isOpen; -</script> - -{#if isOpen} - <div class="search__filters" transition:slide> - <FilterGroup - filterTitle={Drupal.t('Development Status')} - filterData={DEVELOPMENT_OPTIONS} - filterType="developmentStatus" - changeHandler={onAdvancedFilter} - let:id - let:label - > - <label - slot="label" - class="search__checkbox-label" - for={`developmentStatus${id}`} - > - {label} - </label> - </FilterGroup> - <FilterGroup - filterTitle={Drupal.t('Maintenance Status')} - filterData={MAINTENANCE_OPTIONS} - filterType="maintenanceStatus" - changeHandler={onAdvancedFilter} - let:id - let:label - > - <label - slot="label" - class="search__checkbox-label" - for={`maintenanceStatus${id}`} - > - {label} - </label> - </FilterGroup> - <FilterGroup - filterTitle={Drupal.t('Security Advisory Coverage')} - filterData={SECURITY_OPTIONS} - filterType="securityCoverage" - changeHandler={onAdvancedFilter} - let:id - let:label - > - <label - slot="label" - class="search__checkbox-label" - for={`securityCoverage${id}`} - > - {label} - {#if id === COVERED_ID} - <span class="small-icons" /> - {/if} - </label> - </FilterGroup> - </div> -{/if} diff --git a/tests/src/FunctionalJavascript/ProjectBrowserPluginTest.php b/tests/src/FunctionalJavascript/ProjectBrowserPluginTest.php index 6d86b5deb91cef0db83773fff8386f6256eea7eb..1c875f9c811fceac02cea2d7dcbfbc0dfa33fcc5 100644 --- a/tests/src/FunctionalJavascript/ProjectBrowserPluginTest.php +++ b/tests/src/FunctionalJavascript/ProjectBrowserPluginTest.php @@ -15,6 +15,14 @@ class ProjectBrowserPluginTest extends WebDriverTestBase { use ProjectBrowserUiTestTrait; + // Could be moved into trait under PHP 8.3. + protected const SECURITY_OPTION_SELECTOR = 'select[name="securityCoverage"] '; + protected const MAINTENANCE_OPTION_SELECTOR = 'select[name="maintenanceStatus"] '; + protected const DEVELOPMENT_OPTION_SELECTOR = 'select[name="developmentStatus"] '; + protected const OPTION_CHECKED = 'option:checked'; + protected const OPTION_FIRST_CHILD = 'option:first-child'; + protected const OPTION_LAST_CHILD = 'option:last-child'; + /** * {@inheritdoc} */ @@ -97,24 +105,29 @@ class ProjectBrowserPluginTest extends WebDriverTestBase { $this->drupalGet('admin/modules/browse'); $this->svelteInitHelper('text', 'Results'); - // Make sure the second filter applied is the security covered filter. - $this->assertEquals('Covered by a security policy', $this->getElementText('p.filter-applied:last-of-type .filter-applied__label')); - - $this->openAdvancedFilter(); - $security_radio_option_selector = '.filter-group:last-child input:checked ~ label'; - $maintenance_radio_option_selector = '.filter-group:nth-child(2) input:checked ~ label'; - $assert_session->waitForElementVisible('css', $security_radio_option_selector); - $this->assertEquals(trim('Covered by a security policy'), $this->getElementText($security_radio_option_selector)); - $this->assertEquals('Maintained', $this->getElementText($maintenance_radio_option_selector)); + $this->assertEquals('Covered by a security policy', $this->getElementText(self::SECURITY_OPTION_SELECTOR . self::OPTION_CHECKED)); + $this->assertEquals('Maintained', $this->getElementText(self::MAINTENANCE_OPTION_SELECTOR . self::OPTION_CHECKED)); + $this->assertEquals('Show all', $this->getElementText(self::DEVELOPMENT_OPTION_SELECTOR . self::OPTION_CHECKED)); // Clear the security covered filter. - $this->clickWithWait("p.filter-applied:last-of-type > button"); - $this->assertEquals('Show all', $this->getElementText($security_radio_option_selector)); + $this->clickWithWait(self::SECURITY_OPTION_SELECTOR . self::OPTION_LAST_CHILD); + $this->assertEquals('Show all', $this->getElementText(self::SECURITY_OPTION_SELECTOR . self::OPTION_CHECKED)); + + // Set the development status filter. + $this->clickWithWait(self::DEVELOPMENT_OPTION_SELECTOR . self::OPTION_FIRST_CHILD); + $this->assertEquals('Active', $this->getElementText(self::DEVELOPMENT_OPTION_SELECTOR . self::OPTION_CHECKED)); // Clear all filters. $this->pressWithWait('Clear filters'); - $this->assertEquals('Show all', $this->getElementText($security_radio_option_selector)); - $this->assertEquals('Show all', $this->getElementText($security_radio_option_selector)); + $this->assertEquals('Show all', $this->getElementText(self::SECURITY_OPTION_SELECTOR . self::OPTION_CHECKED)); + $this->assertEquals('Show all', $this->getElementText(self::MAINTENANCE_OPTION_SELECTOR . self::OPTION_CHECKED)); + $this->assertEquals('Show all', $this->getElementText(self::DEVELOPMENT_OPTION_SELECTOR . self::OPTION_CHECKED)); + + // Reset to recommended filters. + $this->pressWithWait('Recommended filters'); + $this->assertEquals('Covered by a security policy', $this->getElementText(self::SECURITY_OPTION_SELECTOR . self::OPTION_CHECKED)); + $this->assertEquals('Maintained', $this->getElementText(self::MAINTENANCE_OPTION_SELECTOR . self::OPTION_CHECKED)); + $this->assertEquals('Show all', $this->getElementText(self::DEVELOPMENT_OPTION_SELECTOR . self::OPTION_CHECKED)); } /** diff --git a/tests/src/FunctionalJavascript/ProjectBrowserUiTest.php b/tests/src/FunctionalJavascript/ProjectBrowserUiTest.php index 688b4dc1a09f240dda4ff4cf94b07f4078d2a40d..1b9adabd63e176d9849fbf7cca09660a594cd006 100644 --- a/tests/src/FunctionalJavascript/ProjectBrowserUiTest.php +++ b/tests/src/FunctionalJavascript/ProjectBrowserUiTest.php @@ -26,6 +26,14 @@ class ProjectBrowserUiTest extends WebDriverTestBase { use ProjectBrowserUiTestTrait; + // Could be moved into trait under PHP 8.3. + protected const SECURITY_OPTION_SELECTOR = 'select[name="securityCoverage"] '; + protected const MAINTENANCE_OPTION_SELECTOR = 'select[name="maintenanceStatus"] '; + protected const DEVELOPMENT_OPTION_SELECTOR = 'select[name="developmentStatus"] '; + protected const OPTION_CHECKED = 'option:checked'; + protected const OPTION_FIRST_CHILD = 'option:first-child'; + protected const OPTION_LAST_CHILD = 'option:last-child'; + /** * {@inheritdoc} */ @@ -94,7 +102,7 @@ class ProjectBrowserUiTest extends WebDriverTestBase { } /** - * Tests the clickable category functionality. + * Tests the clickable category functionality on module page. */ public function testClickableCategory(): void { $assert_session = $this->assertSession(); @@ -102,13 +110,15 @@ class ProjectBrowserUiTest extends WebDriverTestBase { $this->drupalGet('admin/modules/browse'); $this->svelteInitHelper('text', 'Dancing Queen'); + + // Click to open module page. $page->clickLink('Dancing Queen'); - $this->svelteInitHelper('text', 'E-commerce'); + $this->svelteInitHelper('text', 'Categories:'); - // Click 'E-commerce' category on module page. + // Click 'E-commerce' lozenge category on module page. $this->clickWithWait('li.pb-module-page__categories-list-item:nth-child(2)'); - $module_category_e_commerce_filter_selector = 'p.filter-applied:nth-child(3)'; - $this->assertEquals('E-commerce', $this->getElementText("$module_category_e_commerce_filter_selector .filter-applied__label")); + $module_category_first_filter_selector = 'p.filter-applied:first-child .filter-applied__label'; + $this->assertEquals('E-commerce', $this->getElementText($module_category_first_filter_selector)); $this->assertTrue($assert_session->waitForText('6 Results')); } @@ -120,12 +130,14 @@ class ProjectBrowserUiTest extends WebDriverTestBase { $assert_session = $this->assertSession(); $this->drupalGet('admin/modules/browse'); - $this->svelteInitHelper('css', '#104'); + $this->svelteInitHelper('css', '.pb-filter__multi-dropdown'); + // Open category drop-down. + $this->clickWithWait('.pb-filter__multi-dropdown', 'E-commerce'); // Click 'E-commerce' checkbox. $this->clickWithWait('#104'); - $module_category_e_commerce_filter_selector = 'p.filter-applied:nth-child(3)'; + $module_category_e_commerce_filter_selector = 'p.filter-applied:first-child'; // Make sure the 'E-commerce' module category filter is applied. $this->assertEquals('E-commerce', $this->getElementText("$module_category_e_commerce_filter_selector .filter-applied__label")); @@ -144,8 +156,15 @@ class ProjectBrowserUiTest extends WebDriverTestBase { 'Astronaut Simulator', ], TRUE); + // Use blur event to close drop-down so Clear is visible. + $this->assertSession()->elementExists('css', '.pb-filter__multi-dropdown')->blur(); + $this->pressWithWait('Clear filters', '25 Results'); + // Open category drop-down again by pressing space. + $this->assertSession()->elementExists('css', '.pb-filter__multi-dropdown')->keyDown(' '); + $this->assertSession()->waitForText('Media'); + // Click 'Media' checkbox. $this->clickWithWait('#67'); @@ -286,6 +305,9 @@ class ProjectBrowserUiTest extends WebDriverTestBase { // shown on a page, the pager has the correct number of items. $this->clickWithWait('[aria-label="First page"]'); + // Open category drop-down. + $this->clickWithWait('.pb-filter__multi-dropdown', 'E-commerce'); + // Click 'Media' checkbox. $this->clickWithWait('#67', '', TRUE); @@ -342,12 +364,11 @@ class ProjectBrowserUiTest extends WebDriverTestBase { 'Astronaut Simulator', ]); - $second_filter_selector = 'p.filter-applied:nth-child(2)'; // Make sure the second filter applied is the security covered filter. - $this->assertEquals('Covered by a security policy', $this->getElementText("$second_filter_selector .filter-applied__label")); + $this->assertEquals('Covered by a security policy', $this->getElementText(self::SECURITY_OPTION_SELECTOR . self::OPTION_CHECKED)); // Clear the security covered filter. - $this->clickWithWait("$second_filter_selector > button"); + $this->clickWithWait('select[name="securityCoverage"] option:last-child'); $this->assertProjectsVisible([ 'Jazz', 'Vitamin&C;$?', @@ -363,19 +384,16 @@ class ProjectBrowserUiTest extends WebDriverTestBase { 'Helvetica', ]); - $this->openAdvancedFilter(); - // Check aria-labelledby property for advanced filter. foreach ($page->findAll('css', '.filters [role="group"]') as $element) { $this->assertSame($element->findAll('xpath', 'div')[0]->getAttribute('id'), $element->getAttribute('aria-labelledby')); } // Click the Active filter. - $assert_session->waitForElementVisible('css', '#developmentStatusactive'); - $this->clickWithWait('#developmentStatusactive'); + $this->clickWithWait(self::DEVELOPMENT_OPTION_SELECTOR . self::OPTION_FIRST_CHILD); // Make sure the correct filter was applied. - $this->assertEquals('Active', $this->getElementText('p.filter-applied:nth-child(1) .filter-applied__label')); + $this->assertEquals('Active', $this->getElementText(self::DEVELOPMENT_OPTION_SELECTOR . self::OPTION_CHECKED)); $this->assertProjectsVisible([ 'Jazz', @@ -393,7 +411,7 @@ class ProjectBrowserUiTest extends WebDriverTestBase { ]); // Click the "Show all" filter for security. - $this->clickWithWait('#securityCoverageall', '', TRUE); + $this->clickWithWait(self::SECURITY_OPTION_SELECTOR . self::OPTION_LAST_CHILD, '', TRUE); $this->assertProjectsVisible([ 'Jazz', 'Cream cheese on a bagel', @@ -413,8 +431,8 @@ class ProjectBrowserUiTest extends WebDriverTestBase { $this->pressWithWait('Clear filters', '25 Results'); // Click the Actively maintained filter. - $this->clickWithWait('#maintenanceStatusmaintained'); - $this->assertEquals('Maintained', $this->getElementText('p.filter-applied:nth-child(1) .filter-applied__label')); + $this->clickWithWait(self::MAINTENANCE_OPTION_SELECTOR . self::OPTION_FIRST_CHILD); + $this->assertEquals('Maintained', $this->getElementText(self::MAINTENANCE_OPTION_SELECTOR . self::OPTION_CHECKED)); $this->assertProjectsVisible([ 'Jazz', @@ -605,14 +623,15 @@ class ProjectBrowserUiTest extends WebDriverTestBase { $this->svelteInitHelper('text', 'Clear Filters'); $this->pressWithWait('Clear filters'); - $this->openAdvancedFilter(); - // Select 'Z-A' sorting order. $this->sortBy('z_a'); // Select the active development status filter. - $assert_session->waitForElementVisible('css', '#developmentStatusactive'); - $this->clickWithWait('#developmentStatusactive'); + $assert_session->waitForElementVisible('css', self::DEVELOPMENT_OPTION_SELECTOR); + $this->clickWithWait(self::DEVELOPMENT_OPTION_SELECTOR . self::OPTION_FIRST_CHILD); + + // Open category drop-down. + $this->clickWithWait('.pb-filter__multi-dropdown', 'E-commerce'); // Select the E-commerce filter. $assert_session->waitForElementVisible('css', '#104'); @@ -654,9 +673,8 @@ class ProjectBrowserUiTest extends WebDriverTestBase { ]); $this->assertTrue($assert_session->waitForText('15 Results')); - $this->assertEquals('Active', $this->getElementText('p.filter-applied:nth-child(1) .filter-applied__label')); - $this->assertEquals('E-commerce', $this->getElementText('p.filter-applied:nth-child(2) .filter-applied__label')); - $this->assertEquals('Media', $this->getElementText('p.filter-applied:nth-child(3) .filter-applied__label')); + $this->assertEquals('E-commerce', $this->getElementText('p.filter-applied:first-child .filter-applied__label')); + $this->assertEquals('Media', $this->getElementText('p.filter-applied:nth-child(2) .filter-applied__label')); $this->clickWithWait('[aria-label="First page"]'); $this->assertProjectsVisible([ @@ -674,9 +692,8 @@ class ProjectBrowserUiTest extends WebDriverTestBase { 'Become a Banana', ], TRUE); - $this->assertEquals('Active', $this->getElementText('p.filter-applied:nth-child(1) .filter-applied__label')); - $this->assertEquals('E-commerce', $this->getElementText('p.filter-applied:nth-child(2) .filter-applied__label')); - $this->assertEquals('Media', $this->getElementText('p.filter-applied:nth-child(3) .filter-applied__label')); + $this->assertEquals('E-commerce', $this->getElementText('p.filter-applied:first-child .filter-applied__label')); + $this->assertEquals('Media', $this->getElementText('p.filter-applied:nth-child(2) .filter-applied__label')); } /** @@ -691,9 +708,9 @@ class ProjectBrowserUiTest extends WebDriverTestBase { $this->pressWithWait('Recommended filters'); // Check that the actively maintained tag is present. - $this->assertEquals('Maintained', $this->getElementText('p.filter-applied:nth-child(1) .filter-applied__label')); + $this->assertEquals('Maintained', $this->getElementText(self::MAINTENANCE_OPTION_SELECTOR . self::OPTION_CHECKED)); // Make sure the second filter applied is the security covered filter. - $this->assertEquals('Covered by a security policy', $this->getElementText('p.filter-applied:nth-child(2) .filter-applied__label')); + $this->assertEquals('Covered by a security policy', $this->getElementText(self::SECURITY_OPTION_SELECTOR . self::OPTION_CHECKED)); $this->assertTrue($assert_session->waitForText('9 Results')); } @@ -722,12 +739,18 @@ class ProjectBrowserUiTest extends WebDriverTestBase { $this->pressWithWait('Clear filters', '25 Results'); + // Open category drop-down. + $this->clickWithWait('.pb-filter__multi-dropdown', 'E-commerce', TRUE); + // Click 'E-commerce' checkbox. $this->clickWithWait('#104'); // Click 'Media' checkbox. $this->clickWithWait('#67', '20 Results'); + // Use blur event to close drop-down so Clear is visible. + $this->assertSession()->elementExists('css', '.pb-filter__multi-dropdown')->blur(); + // Filter by search text. $this->inputSearchField('Number'); $this->assertTrue($assert_session->waitForText('2 Results')); @@ -745,8 +768,12 @@ class ProjectBrowserUiTest extends WebDriverTestBase { $assert_session->waitForElementVisible('css', '#project-browser .pb-project'); $result_count_text = $page->find('css', '.pb-search-results')->getText(); $this->assertNotEquals('9 Results Sorted by Active installs', $result_count_text); + + // Open category drop-down again by pressing space. + $this->assertSession()->elementExists('css', '.pb-filter__multi-dropdown')->keyDown(' '); + // Apply the second module category filter. - $second_category_filter_selector = '#project-browser > div.pb-layout > .pb-layout__aside > div > form > section > details > fieldset > label:nth-child(3)'; + $second_category_filter_selector = '.pb-filter__multi-dropdown__items > .pb-filter__checkbox-label:nth-child(3) input'; $this->clickWithWait("$second_category_filter_selector"); // Save the filter applied in second tab. @@ -928,6 +955,9 @@ class ProjectBrowserUiTest extends WebDriverTestBase { 'Astronaut Simulator', ]); + // Open category drop-down. + $this->clickWithWait('.pb-filter__multi-dropdown', 'E-commerce', TRUE); + // Click 'Media' checkbox. $this->clickWithWait('#67'); $this->assertPagerItems(['1', '2', 'Next', 'Last']); diff --git a/tests/src/FunctionalJavascript/ProjectBrowserUiTestJsonApi.php b/tests/src/FunctionalJavascript/ProjectBrowserUiTestJsonApi.php index bd2649e17805182a2e2257751c56f931949cdafb..32e0c8dc1ebe626fb74989416c22ce67356df354 100644 --- a/tests/src/FunctionalJavascript/ProjectBrowserUiTestJsonApi.php +++ b/tests/src/FunctionalJavascript/ProjectBrowserUiTestJsonApi.php @@ -19,6 +19,14 @@ class ProjectBrowserUiTestJsonApi extends WebDriverTestBase { use ProjectBrowserUiTestTrait; + // Could be moved into trait under PHP 8.3. + protected const SECURITY_OPTION_SELECTOR = 'select[name="securityCoverage"] '; + protected const MAINTENANCE_OPTION_SELECTOR = 'select[name="maintenanceStatus"] '; + protected const DEVELOPMENT_OPTION_SELECTOR = 'select[name="developmentStatus"] '; + protected const OPTION_CHECKED = 'option:checked'; + protected const OPTION_FIRST_CHILD = 'option:first-child'; + protected const OPTION_LAST_CHILD = 'option:last-child'; + /** * {@inheritdoc} */ @@ -80,8 +88,8 @@ class ProjectBrowserUiTestJsonApi extends WebDriverTestBase { $assert_session = $this->assertSession(); $this->drupalGet('admin/modules/browse'); - $this->svelteInitHelper('css', '.pb-filter__categories input[type="checkbox"]'); - $assert_session->elementsCount('css', '.pb-filter__categories input[type="checkbox"]', 54); + $this->svelteInitHelper('css', '.pb-filter__multi-dropdown input[type="checkbox"]'); + $assert_session->elementsCount('css', '.pb-filter__multi-dropdown input[type="checkbox"]', 54); } /** @@ -98,7 +106,7 @@ class ProjectBrowserUiTestJsonApi extends WebDriverTestBase { // Click 'Utility' category on module page. $this->clickWithWait('.pb-module-page__details .pb-module-page__categories-list li:nth-child(2)'); - $module_category_utility_filter_selector = 'p.filter-applied:nth-child(3)'; + $module_category_utility_filter_selector = 'p.filter-applied:nth-child(1)'; $this->assertEquals('Utility', $this->getElementText("$module_category_utility_filter_selector .filter-applied__label")); $this->assertTrue($assert_session->waitForText('732 Results')); } @@ -110,12 +118,18 @@ class ProjectBrowserUiTestJsonApi extends WebDriverTestBase { $assert_session = $this->assertSession(); $this->drupalGet('admin/modules/browse'); - $this->svelteInitHelper('css', '#acc38507-ac85-43e6-8f32-beb3febea93f'); + $this->svelteInitHelper('css', '.pb-filter__multi-dropdown'); + // Open category drop-down. + $this->clickWithWait('.pb-filter__multi-dropdown', 'E-commerce', TRUE); + $this->svelteInitHelper('css', '#acc38507-ac85-43e6-8f32-beb3febea93f'); // Click 'E-commerce' checkbox. $this->clickWithWait('#acc38507-ac85-43e6-8f32-beb3febea93f'); - $module_category_e_commerce_filter_selector = 'p.filter-applied:nth-child(3)'; + // Use blur event to close drop-down so Clear is visible. + $this->assertSession()->elementExists('css', '.pb-filter__multi-dropdown')->blur(); + + $module_category_e_commerce_filter_selector = 'p.filter-applied:nth-child(1)'; // Make sure the 'E-commerce' module category filter is applied. $this->assertEquals('E-commerce', $this->getElementText("$module_category_e_commerce_filter_selector .filter-applied__label")); @@ -131,6 +145,9 @@ class ProjectBrowserUiTestJsonApi extends WebDriverTestBase { $this->pressWithWait('Clear filters', '197 Results'); + // Open category drop-down. + $this->clickWithWait('.pb-filter__multi-dropdown', 'E-commerce', TRUE); + // Click 'Media' checkbox. $this->clickWithWait('#ee0200ec-4920-411e-9768-2cc588deaa38'); @@ -200,6 +217,9 @@ class ProjectBrowserUiTestJsonApi extends WebDriverTestBase { // shown on a page, the pager has the correct number of items. $this->clickWithWait('[aria-label="First page"]'); + // Open category drop-down. + $this->clickWithWait('.pb-filter__multi-dropdown', 'E-commerce'); + // Click 'Commerce/Advertising' checkbox. $this->clickWithWait('#23d470f6-ffde-4034-a6ef-492b7121b9cf', '', TRUE); @@ -225,32 +245,29 @@ class ProjectBrowserUiTestJsonApi extends WebDriverTestBase { 'Chaos Tool Suite (ctools)', 'Token', 'Pathauto', 'Libraries API', 'Entity API', 'Webform', 'Metatag', 'Field Group', 'IMCE', 'CAPTCHA', 'Google Analytics', 'Redirect', ]); - $second_filter_selector = 'p.filter-applied:nth-child(2)'; // Make sure the second filter applied is the security covered filter. - $this->assertEquals('Covered by a security policy', $this->getElementText("$second_filter_selector .filter-applied__label")); + $this->assertEquals('Covered by a security policy', $this->getElementText(self::SECURITY_OPTION_SELECTOR . self::OPTION_CHECKED)); // Clear the security covered filter. - $this->clickWithWait("$second_filter_selector > button"); + $this->clickWithWait(self::SECURITY_OPTION_SELECTOR . self::OPTION_LAST_CHILD); $this->assertProjectsVisible([ 'Chaos Tool Suite (ctools)', 'Token', 'Pathauto', 'Libraries API', 'Entity API', 'Webform', 'Metatag', 'Field Group', 'IMCE', 'CAPTCHA', 'Google Analytics', 'Redirect', ]); - $this->openAdvancedFilter(); - // Click the Active filter. - $assert_session->waitForElementVisible('css', '#developmentStatusactive'); - $this->clickWithWait('#developmentStatusactive'); + $assert_session->waitForElementVisible('css', self::DEVELOPMENT_OPTION_SELECTOR); + $this->clickWithWait(self::DEVELOPMENT_OPTION_SELECTOR . self::OPTION_FIRST_CHILD); // Make sure the correct filter was applied. - $this->assertEquals('Active', $this->getElementText('p.filter-applied:nth-child(1) .filter-applied__label')); + $this->assertEquals('Active', $this->getElementText(self::DEVELOPMENT_OPTION_SELECTOR . self::OPTION_CHECKED)); $assert_session->waitForText('No records available'); // Clear all filters. $this->pressWithWait('Clear filters', 'Results'); // Click the Actively maintained filter. - $this->clickWithWait('#maintenanceStatusmaintained', '6,749 Results'); - $this->assertEquals('Maintained', $this->getElementText('p.filter-applied:nth-child(1) .filter-applied__label')); + $this->clickWithWait(self::MAINTENANCE_OPTION_SELECTOR . self::OPTION_FIRST_CHILD, '6,749 Results'); + $this->assertEquals('Maintained', $this->getElementText(self::MAINTENANCE_OPTION_SELECTOR . self::OPTION_CHECKED)); $this->assertProjectsVisible([ 'Chaos Tool Suite (ctools)', 'Token', 'Pathauto', 'Libraries API', 'Entity API', 'Webform', 'Metatag', 'Field Group', 'IMCE', 'CAPTCHA', 'Google Analytics', 'Redirect', @@ -338,9 +355,9 @@ class ProjectBrowserUiTestJsonApi extends WebDriverTestBase { $this->pressWithWait('Recommended filters'); // Check that the actively maintained tag is present. - $this->assertEquals('Maintained', $this->getElementText('p.filter-applied:nth-child(1) .filter-applied__label')); + $this->assertEquals('Maintained', $this->getElementText(self::MAINTENANCE_OPTION_SELECTOR . self::OPTION_CHECKED)); // Make sure the second filter applied is the security covered filter. - $this->assertEquals('Covered by a security policy', $this->getElementText('p.filter-applied:nth-child(2) .filter-applied__label')); + $this->assertEquals('Covered by a security policy', $this->getElementText(self::SECURITY_OPTION_SELECTOR . self::OPTION_CHECKED)); $this->assertTrue($assert_session->waitForText('4,523 Results')); } @@ -354,8 +371,8 @@ class ProjectBrowserUiTestJsonApi extends WebDriverTestBase { $this->container->get('module_installer')->install(['project_browser_devel']); // Test categories with multiple plugin enabled. $this->drupalGet('admin/modules/browse'); - $this->svelteInitHelper('css', '.pb-filter__categories input[type="checkbox"]'); - $assert_session->elementsCount('css', '.pb-filter__categories input[type="checkbox"]', 54); + $this->svelteInitHelper('css', '.pb-filter__multi-dropdown input[type="checkbox"]'); + $assert_session->elementsCount('css', '.pb-filter__multi-dropdown input[type="checkbox"]', 54); $this->svelteInitHelper('css', '#project-browser .pb-project'); // Count tabs. @@ -369,12 +386,18 @@ class ProjectBrowserUiTestJsonApi extends WebDriverTestBase { $this->pressWithWait('Clear filters', '7,236 Results'); + // Open category drop-down. + $this->clickWithWait('.pb-filter__multi-dropdown', 'E-commerce', TRUE); + // Click 'E-commerce' checkbox. $this->clickWithWait('#acc38507-ac85-43e6-8f32-beb3febea93f'); // Click 'Commerce/Advertising' checkbox. $this->clickWithWait('#23d470f6-ffde-4034-a6ef-492b7121b9cf', '557 Results'); + // Use blur event to close drop-down so Clear is visible. + $this->assertSession()->elementExists('css', '.pb-filter__multi-dropdown')->blur(); + // Filter by search text. $this->inputSearchField('th'); $this->assertTrue($assert_session->waitForText('2 Results')); @@ -384,12 +407,16 @@ class ProjectBrowserUiTestJsonApi extends WebDriverTestBase { // Click other tab. $this->pressWithWait('random_data'); - $this->svelteInitHelper('css', '.pb-filter__categories input[type="checkbox"]'); - $assert_session->elementsCount('css', '.pb-filter__categories input[type="checkbox"]', 20); $assert_session->waitForElementVisible('css', '#project-browser .pb-project'); + // Open category drop-down. + $this->clickWithWait('.pb-filter__multi-dropdown', '', TRUE); + + $this->svelteInitHelper('css', '.pb-filter__multi-dropdown input[type="checkbox"]'); + $assert_session->elementsCount('css', '.pb-filter__multi-dropdown input[type="checkbox"]', 20); + // Apply the second module category filter. - $second_category_filter_selector = '#project-browser > div.pb-layout > .pb-layout__aside > div > form > section > details > fieldset > label:nth-child(2)'; + $second_category_filter_selector = '.pb-filter__multi-dropdown .pb-filter__checkbox-label:nth-child(2) input[type="checkbox"]'; $this->clickWithWait("$second_category_filter_selector"); // Save the filter applied in second tab. @@ -490,8 +517,8 @@ class ProjectBrowserUiTestJsonApi extends WebDriverTestBase { // Verify that only Random data plugin is enabled. $this->drupalGet('admin/modules/browse'); - $this->svelteInitHelper('css', '.pb-filter__categories input[type="checkbox"]'); - $assert_session->elementsCount('css', '.pb-filter__categories input[type="checkbox"]', 20); + $this->svelteInitHelper('css', '.pb-filter__multi-dropdown input[type="checkbox"]'); + $assert_session->elementsCount('css', '.pb-filter__multi-dropdown input[type="checkbox"]', 20); // Enable only Drupal.org mockapi plugin through config update. It is done // this way because dragging was not working reliably for enabling @@ -503,8 +530,8 @@ class ProjectBrowserUiTestJsonApi extends WebDriverTestBase { // Verify that only Drupal.org mockapi plugin is enabled. $this->drupalGet('admin/modules/browse'); - $this->svelteInitHelper('css', '.pb-filter__categories input[type="checkbox"]'); - $assert_session->elementsCount('css', '.pb-filter__categories input[type="checkbox"]', 19); + $this->svelteInitHelper('css', '.pb-filter__multi-dropdown input[type="checkbox"]'); + $assert_session->elementsCount('css', '.pb-filter__multi-dropdown input[type="checkbox"]', 19); } } diff --git a/tests/src/FunctionalJavascript/TranslatedSvelteAppTest.php b/tests/src/FunctionalJavascript/TranslatedSvelteAppTest.php index 6420418ca5c82548c4ab9fdf7f497bae196f2d1c..005ad4ef6cb6c5ce1fde5f9b91c0886854bbb211 100644 --- a/tests/src/FunctionalJavascript/TranslatedSvelteAppTest.php +++ b/tests/src/FunctionalJavascript/TranslatedSvelteAppTest.php @@ -71,7 +71,7 @@ class TranslatedSvelteAppTest extends WebDriverTestBase { $translate_to = 'Soorch Foor Moodools'; $this->drupalGet('admin/modules/browse'); - $this->svelteInitHelper('text', 'Search for modules'); + $this->svelteInitHelper('text', 'Keyword search'); $this->assertFalse($this->assertSession()->waitForText($translate_to)); // This forces locale JS string sources to be imported. @@ -80,12 +80,12 @@ class TranslatedSvelteAppTest extends WebDriverTestBase { // Translate a string in locale.admin.js to our new language. $strings = \Drupal::service('locale.storage') ->getStrings([ - 'source' => 'Search for modules', + 'source' => 'Keyword search', ]); $string = $strings[0]; - $this->submitForm(['string' => 'Search for modules'], 'Filter'); + $this->submitForm(['string' => 'Keyword search'], 'Filter'); $edit = ['strings[' . $string->lid . '][translations][0]' => $translate_to]; $this->submitForm($edit, 'Save translations'); $this->drupalGet("/$prefix/admin/modules/browse"); diff --git a/tests/src/Nightwatch/Tests/keyboardTest.js b/tests/src/Nightwatch/Tests/keyboardTest.js new file mode 100644 index 0000000000000000000000000000000000000000..16e220f8a61f19713397c738c99d5ff50bb64389 --- /dev/null +++ b/tests/src/Nightwatch/Tests/keyboardTest.js @@ -0,0 +1,208 @@ +const delayInMilliseconds = 100; +const filterKeywordSearch = '#pb-text'; +const filterDropdownSelector = '.pb-filter__multi-dropdown'; + +module.exports = { + '@tags': ['project_browser'], + before(browser) { + browser + .drupalInstall() + .drupalInstallModule('project_browser') + .drupalInstallModule('project_browser_test'); + }, + after(browser) { + browser.drupalUninstall(); + }, + 'Project browser categories': (browser) => { + const assertFocus = (selector, message) => { + browser.execute( + // eslint-disable-next-line func-names, prefer-arrow-callback, no-shadow + function (selector) { + return document.activeElement.matches(selector); + }, + [selector], + (result) => { + browser.assert.ok(result.value, message); + }, + ); + }; + function sendDownKey() { + return this.actions().sendKeys(browser.Keys.ARROW_DOWN); + } + browser.drupalLoginAsAdmin(() => { + // Open project browser settings page, enable mock API, and disable + // drupal.org and recipes. + browser + .drupalRelativeURL('/admin/config/development/project_browser') + .waitForElementVisible('#edit-enabled-sources-drupalorg-mockapi-status') + .updateValue( + '[data-drupal-selector="edit-enabled-sources-drupalorg-mockapi-status"]', + 'enabled', + ) + .updateValue( + '[data-drupal-selector="edit-enabled-sources-drupalorg-jsonapi-status"]', + 'disabled', + ) + // Check for presence of Recipe option, as won't exist for < Drupal 10. + .element( + 'css selector', + '#edit-enabled-sources-recipes-status', + function disableRecipes(result) { + if (result.status !== -1) { + browser.updateValue( + '[data-drupal-selector="edit-enabled-sources-recipes-status"]', + 'disabled', + ); + } + }, + ) + .click('[data-drupal-selector="edit-submit"]') + .waitForElementVisible('[data-drupal-messages]') + .assert.textContains( + '[data-drupal-messages]', + 'The configuration options have been saved', + ); + + // Open project browser. + browser + .drupalRelativeURL('/admin/modules/browse') + .waitForElementVisible('h1', delayInMilliseconds) + .assert.textContains('h1', 'Browse projects') + .waitForElementVisible(filterDropdownSelector); + + // Use mouse to get to search box, and verify it has active focus. + browser.click(filterKeywordSearch); + assertFocus(filterKeywordSearch, 'Assert search box has focus.'); + + // Press tab to navigate to categories drop-down. + browser + .keys(browser.Keys.TAB) + .pause(delayInMilliseconds) + .assert.textEquals( + '.pb-filter__multi-dropdown__label', + 'Select categories', + 'Assert that drop-down label has initial select message.', + ) + .assert.not.visible( + '.pb-filter__multi-dropdown__items', + 'Assert that category checkboxes are hidden.', + ); + assertFocus( + filterDropdownSelector, + 'Assert that focus moves to category drop-down on tab.', + ); + + // Press space to expand categories drop-down, verify focus moves to first + // checkbox control. + browser + .keys(browser.Keys.SPACE) + .assert.visible( + '.pb-filter__multi-dropdown__items', + 'Assert category checkboxes are now visible.', + ) + .pause(1000); + assertFocus( + '.pb-filter__checkbox-label:first-child .pb-filter__checkbox', + 'Assert that first category checkbox has focus.', + ); + + // Press down arrow key, verify focus moves to next checkbox. + browser.perform(sendDownKey).pause(delayInMilliseconds); + assertFocus( + '.pb-filter__checkbox-label:nth-child(2) .pb-filter__checkbox', + 'Assert that second category checkbox has focus.', + ); + + // Press space key. Verify active checkbox checked. + browser + .keys(browser.Keys.SPACE) + .pause(delayInMilliseconds) + .assert.selected( + '.pb-filter__checkbox-label:nth-child(2) .pb-filter__checkbox', + 'Assert second category is selected.', + ); + + // Press escape key. Verify category drop-down closes and focus goes to + // overall drop-down. + browser + .keys(browser.Keys.ESCAPE) + .pause(delayInMilliseconds) + .assert.not.visible( + '.pb-filter__checkbox', + 'Assert category checkboxes are hidden again.', + ) + .assert.textEquals( + '.pb-filter__multi-dropdown__label', + 'Accessibility', + 'Assert that Accessibility on drop-down label.', + ); + assertFocus( + filterDropdownSelector, + 'Assert that focus is back on drop-down list.', + ); + + // Verify Accessibility lozenge shown. + browser.assert.textEquals( + '.filter-applied:first-child .filter-applied__label', + 'Accessibility', + 'Assert that Accessibility lozenge is shown.', + ); + + // Press space to expand categories drop-down again. + browser + .keys(browser.Keys.SPACE) + .pause(delayInMilliseconds) + .assert.visible( + '.pb-filter__multi-dropdown__items', + 'Assert category checkboxes are now visible.', + ); + assertFocus( + '.pb-filter__checkbox-label:first-child .pb-filter__checkbox', + 'Assert that first category checkbox has focus.', + ); + + // Press down arrow key to move to second checkbox. + browser.perform(sendDownKey).pause(delayInMilliseconds); + assertFocus( + '.pb-filter__checkbox-label:nth-child(2) .pb-filter__checkbox', + 'Assert that second category checkbox has focus.', + ); + + // Press space key. Verify checkbox cleared. + browser + .keys(browser.Keys.SPACE) + .pause(delayInMilliseconds) + .assert.not.selected( + '.pb-filter__checkbox-label:nth-child(2) .pb-filter__checkbox', + 'Assert second category is selected.', + ); + + // Press space to expand categories drop-down again. + browser + .keys(browser.Keys.TAB) + .pause(delayInMilliseconds) + .assert.textEquals( + '.pb-filter__multi-dropdown__label', + 'Select categories', + 'Assert that drop-down label has initial select message.', + ) + .assert.not.visible( + '.pb-filter__checkbox', + 'Assert category checkboxes are hidden again.', + ); + assertFocus( + '.filter-group__filter-options:first-of-type .search__filter-select', + 'Assert that focus has moved to next filter drop-down.', + ); + + // Verify Accessibility lozenge shown. + browser.assert.not.elementPresent( + '.filter-applied:first-child .filter-applied__label', + 'Assert that no filter lozenge.', + ); + + // Close out test. + browser.drupalLogAndEnd({ onlyOnError: false }); + }); + }, +};