diff --git a/src/Controller/BrowserController.php b/src/Controller/BrowserController.php index b785f5e7f392f49ea2460647da67fe0fce2859f1..3c0e05030287cfb0b19da6f7960edb74d54809e2 100644 --- a/src/Controller/BrowserController.php +++ b/src/Controller/BrowserController.php @@ -132,9 +132,10 @@ class BrowserController extends ControllerBase { // To get common data from single source plugin. $current_source = reset($current_sources); - $sort_options = []; + $sort_options = $active_plugins = []; foreach ($current_sources as $source) { $sort_options[$source->getPluginId()] = array_values($source->getSortOptions()); + $active_plugins[$source->getPluginId()] = $source->getPluginDefinition()['label']; } return [ @@ -145,6 +146,7 @@ class BrowserController extends ControllerBase { ], 'drupalSettings' => [ 'project_browser' => [ + 'active_plugins' => $active_plugins, 'modules' => $modules_status, 'drupal_version' => \Drupal::VERSION, 'drupal_core_compatibility' => \Drupal::CORE_COMPATIBILITY, diff --git a/sveltejs/public/build/bundle.css b/sveltejs/public/build/bundle.css index fafdf31ddab767ea9f5726ed35690f1b868b9d3d..b3af48a31efa4e366e4f8acf253ce04396f92266 100644 Binary files a/sveltejs/public/build/bundle.css and b/sveltejs/public/build/bundle.css differ diff --git a/sveltejs/public/build/bundle.js b/sveltejs/public/build/bundle.js index 98495ee24d800de4ca78790a57dab725bb8585ef..b50a1275160b7c155751ee1f2eeb1d2897d44d57 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 a1b470754c331e7e0a63a348cc177ef1cce57ef1..2e4d71aaada25084f66b225475bc10770b1c1a1a 100644 Binary files a/sveltejs/public/build/bundle.js.map and b/sveltejs/public/build/bundle.js.map differ diff --git a/sveltejs/src/ProjectBrowser.svelte b/sveltejs/src/ProjectBrowser.svelte index 5e78168d8c5b28cc1d9a3184f57a301909774b77..a68dfa4eb6d8ff477246a01edec6494ff0206844 100644 --- a/sveltejs/src/ProjectBrowser.svelte +++ b/sveltejs/src/ProjectBrowser.svelte @@ -33,10 +33,12 @@ MODULE_STATUS, ALLOW_UI_INSTALL, PM_VALIDATION_ERROR, + ACTIVE_PLUGINS, } from './constants'; // cspell:ignore tabwise const { Drupal } = window; + const { announce } = Drupal; let data; let rows = []; @@ -45,6 +47,7 @@ const pageIndex = 0; // first row let loading = true; + let sortText = $sortCriteria.find((option) => option.id === $sort).text; // eslint-disable-next-line import/no-mutable-exports,import/prefer-default-export export let searchText; searchString.subscribe((value) => { @@ -61,6 +64,7 @@ element = value; }); let filterComponent; + let searchComponent; /** * Load data from Drupal.org API. @@ -184,6 +188,7 @@ } async function onSort(event) { sort.set(event.detail.sort); + sortText = $sortCriteria.find((option) => option.id === $sort).text; await load(0); page.set(0); } @@ -223,6 +228,34 @@ await load(0); } + /** + * Refreshes the live region after a filter or search completes. + */ + const refreshLiveRegion = () => { + if ($rowsCount) { + // Set announce() to an empty string. This ensures the result count will + // be announced after filtering even if the count is the same. + announce(''); + + // The announcement is delayed by 210 milliseconds, a wait that is + // slightly longer than the 200 millisecond debounce() built into + // announce(). This ensures that the above call to reset the aria live + // region to an empty string actually takes place instead of being + // debounced. + setTimeout(() => { + announce( + Drupal.t('@count Results for @active_tab, Sorted by @sortText', { + '@count': $rowsCount + .toString() + .replace(/\B(?=(\d{3})+(?!\d))/g, ','), + '@sortText': sortText, + '@active_tab': ACTIVE_PLUGINS[$activeTab], + }), + ); + }, 210); + } + }; + document.onmouseover = function setInnerDocClickTrue() { window.innerDocClick = true; }; @@ -253,48 +286,66 @@ <ProjectGrid {loading} {rows} {pageIndex} {$pageSize} let:rows> <div slot="top"> <Search + bind:this={searchComponent} on:search={onSearch} on:sort={onSort} on:advancedFilter={onAdvancedFilter} on:selectCategory={onSelectCategory} {searchText} + {refreshLiveRegion} /> - {#if matches} - <div class="project-browser__toggle-buttons"> - <button - class:project-browser__selected-tab={toggleView === 'List'} - class="project-browser__toggle project-browser__list-button" - value="List" - on:click={(e) => { - toggleView = 'List'; - onToggle(e.target.value); - }} - > - <img - class="project-browser__list-icon" - src="{FULL_MODULE_PATH}/images/list.svg" - alt="" - /> - {Drupal.t('List')} - </button> - <button - class:project-browser__selected-tab={toggleView === 'Grid'} - class="project-browser__toggle project-browser__grid-button" - value="Grid" - on:click={(e) => { - toggleView = 'Grid'; - onToggle(e.target.value); - }} - > - <img - class="project-browser__grid-icon" - src="{FULL_MODULE_PATH}/images/grid-fill.svg" - alt="" - /> - {Drupal.t('Grid')} - </button> + + <div class="search-results-wrapper"> + <div class="search-results"> + {#each dataArray as dataValue} + {#if $activeTab === dataValue.pluginId} + <span id="output"> + {$rowsCount && + $rowsCount.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',')} + {Drupal.t('Results')} + </span> + {/if} + {/each} </div> - {/if} + + {#if matches} + <div class="project-browser__toggle-buttons"> + <button + class:project-browser__selected-tab={toggleView === 'List'} + class="project-browser__toggle project-browser__list-button" + value="List" + on:click={(e) => { + toggleView = 'List'; + onToggle(e.target.value); + }} + > + <img + class="project-browser__list-icon" + src="{FULL_MODULE_PATH}/images/list.svg" + alt="" + /> + {Drupal.t('List')} + </button> + <button + class:project-browser__selected-tab={toggleView === 'Grid'} + class="project-browser__toggle project-browser__grid-button" + value="Grid" + on:click={(e) => { + toggleView = 'Grid'; + onToggle(e.target.value); + }} + > + <img + class="project-browser__grid-icon" + src="{FULL_MODULE_PATH}/images/grid-fill.svg" + alt="" + /> + {Drupal.t('Grid')} + </button> + </div> + {/if} + </div> + {#if dataArray.length >= 2} <nav aria-label={Drupal.t('Plugin tabs')}> <div class="project-browser__plugin-tabs"> @@ -306,11 +357,10 @@ value={dataValue.pluginId} on:click={(e) => { toggleRows(e.target.value); + searchComponent.onSearch(e); }} > {dataValue.pluginLabel} - {dataValue.totalResults} - {Drupal.t('Results')} </button> {/each} </div> @@ -382,6 +432,7 @@ .project-browser__toggle-buttons { display: flex; margin-inline-end: 25px; + font-weight: bold; } .project-browser__toggle:focus { box-shadow: 0 0 0 2px #fff, 0 0 0 5px #26a769; @@ -413,6 +464,11 @@ .project-browser__plugin-tabs .project-browser__toggle { margin-inline-start: 0; } + .search-results { + font-weight: bold; + margin-inline-start: 10px; + margin-bottom: 5px; + } .project-browser__install-warning { border: 1px solid red; padding: 1em; @@ -423,6 +479,20 @@ .project-browser__warning-header { color: red; } + .search-results-wrapper { + display: flex; + justify-content: space-between; + } + #output { + display: inline-block; + font-family: sans-serif; + font-style: normal; + font-weight: 700; + font-size: 14px; + line-height: 21px; + margin-left: 20px; + } + @media (forced-colors: active) { .project-browser__toggle { border: 1px solid; diff --git a/sveltejs/src/Search/Search.svelte b/sveltejs/src/Search/Search.svelte index ece982f63a9fd58a297a4cacac66231ee87c850a..9e8d01f2ed288d7e8aa70e4105a7bd85baa3cc67 100644 --- a/sveltejs/src/Search/Search.svelte +++ b/sveltejs/src/Search/Search.svelte @@ -7,7 +7,6 @@ import SearchSort from './SearchSort.svelte'; import { filters, - rowsCount, filtersVocabularies, moduleCategoryFilter, moduleCategoryVocabularies, @@ -28,10 +27,10 @@ // cspell:ignore searchterm const { Drupal } = window; - const { announce } = Drupal; const dispatch = createEventDispatcher(); const stateContext = getContext('state'); + export let refreshLiveRegion; export const filter = (row, text) => Object.values(row).filter( (item) => @@ -55,33 +54,6 @@ } let sortText = sortMatch.text; - /** - * Refreshes the live region after a filter or search completes. - */ - const refreshLiveRegion = () => { - if ($rowsCount) { - // Set announce() to an empty string. This ensures the result count will - // be announced after filtering even if the count is the same. - announce(''); - - // The announcement is delayed by 210 milliseconds, a wait that is - // slightly longer than the 200 millisecond debounce() built into - // announce(). This ensures that the above call to reset the aria live - // region to an empty string actually takes place instead of being - // debounced. - setTimeout(() => { - announce( - Drupal.t('@count Results, Sorted by @sortText', { - '@count': $rowsCount - .toString() - .replace(/\B(?=(\d{3})+(?!\d))/g, ','), - '@sortText': sortText, - }), - ); - }, 210); - } - }; - const updateVocabularies = (vocabulary, value) => { const normalizedValue = normalizeOptions(value); const storedValue = JSON.parse(localStorage.getItem(`pb.${vocabulary}`)); @@ -97,7 +69,7 @@ updateVocabularies('securityCoverage', SECURITY_OPTIONS); }); - async function onSearch(event) { + export async function onSearch(event) { const state = stateContext.getState(); const detail = { originalEvent: event, @@ -219,14 +191,6 @@ > <section aria-label={Drupal.t('Search results')}> <div class="search__results-count"> - <span id="output"> - {$rowsCount && - $rowsCount.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',')} - {Drupal.t('Results')} - <span class="visually-hidden" - >{Drupal.t('Sorted by @sortText', { '@sortText': sortText })}</span - > - </span> {#each ['developmentStatus', 'maintenanceStatus', 'securityCoverage'] as filterType} {#if $filters[filterType]} <FilterApplied @@ -335,16 +299,6 @@ z-index: 1; } - #output { - display: inline-block; - font-family: sans-serif; - font-style: normal; - font-weight: 700; - font-size: 14px; - line-height: 21px; - margin-inline-start: 20px; - } - .search__grid-container { display: grid; height: auto; diff --git a/sveltejs/src/constants.js b/sveltejs/src/constants.js index d977be40835410710028cfc4d660e78d9f6fad37..7f204cdd6651450b3b01c7034f0682cf510ee2dc 100644 --- a/sveltejs/src/constants.js +++ b/sveltejs/src/constants.js @@ -22,3 +22,4 @@ export const DARK_COLOR_SCHEME = matchMedia('(forced-colors: active)').matches && matchMedia('(prefers-color-scheme: dark)').matches; export const PM_VALIDATION_ERROR = drupalSettings.project_browser.pm_validation; +export const ACTIVE_PLUGINS = drupalSettings.project_browser.active_plugins; diff --git a/tests/src/FunctionalJavascript/ProjectBrowserUiTest.php b/tests/src/FunctionalJavascript/ProjectBrowserUiTest.php index 3941545856adbb7d822c8ecec7f7c7937c252fdf..47df202a2ae245463d4e68765ae3931699a491ba 100644 --- a/tests/src/FunctionalJavascript/ProjectBrowserUiTest.php +++ b/tests/src/FunctionalJavascript/ProjectBrowserUiTest.php @@ -154,8 +154,8 @@ class ProjectBrowserUiTest extends WebDriverTestBase { $this->svelteInitHelper('text', 'E-commerce'); // Click 'E-commerce' category on module page. - $this->clickWithWait('#project-browser li:nth-child(2)'); - $module_category_e_commerce_filter_selector = 'p.filter-applied:nth-child(4)'; + $this->clickWithWait('li.module-page__category-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")); $this->assertTrue($assert_session->waitForText('6 Results')); } @@ -173,7 +173,7 @@ class ProjectBrowserUiTest extends WebDriverTestBase { // Click 'E-commerce' checkbox. $this->clickWithWait('#104'); - $module_category_e_commerce_filter_selector = 'p.filter-applied:nth-child(4)'; + $module_category_e_commerce_filter_selector = 'p.filter-applied:nth-child(3)'; // 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")); @@ -201,7 +201,7 @@ class ProjectBrowserUiTest extends WebDriverTestBase { $this->clickWithWait('#55'); // Make sure the 'Media' module category filter is applied. - $this->assertEquals('Media', $this->getElementText('p.filter-applied:nth-child(3) .filter-applied__label')); + $this->assertEquals('Media', $this->getElementText('p.filter-applied:nth-child(2) .filter-applied__label')); // Assert that only media and administration module categories are shown. $this->assertProjectsVisible([ 'Jazz', @@ -387,7 +387,7 @@ class ProjectBrowserUiTest extends WebDriverTestBase { 'Astronaut Simulator', ]); - $second_filter_selector = 'p.filter-applied:nth-child(3)'; + $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")); @@ -420,7 +420,7 @@ class ProjectBrowserUiTest extends WebDriverTestBase { $this->clickWithWait('#developmentStatusactive'); // Make sure the correct filter was applied. - $this->assertEquals('Active', $this->getElementText('p.filter-applied:nth-child(2) .filter-applied__label')); + $this->assertEquals('Active', $this->getElementText('p.filter-applied:nth-child(1) .filter-applied__label')); $this->assertProjectsVisible([ 'Jazz', @@ -459,7 +459,7 @@ class ProjectBrowserUiTest extends WebDriverTestBase { // Click the Actively maintained filter. $this->clickWithWait('#maintenanceStatusmaintained'); - $this->assertEquals('Maintained', $this->getElementText('p.filter-applied:nth-child(2) .filter-applied__label')); + $this->assertEquals('Maintained', $this->getElementText('p.filter-applied:nth-child(1) .filter-applied__label')); $this->assertProjectsVisible([ 'Jazz', @@ -704,9 +704,9 @@ class ProjectBrowserUiTest extends WebDriverTestBase { ]); $this->assertTrue($assert_session->waitForText('16 Results')); - $this->assertEquals('Active', $this->getElementText('p.filter-applied:nth-child(2) .filter-applied__label')); - $this->assertEquals('Commerce/Advertising', $this->getElementText('p.filter-applied:nth-child(3) .filter-applied__label')); - $this->assertEquals('Media', $this->getElementText('p.filter-applied:nth-child(4) .filter-applied__label')); + $this->assertEquals('Active', $this->getElementText('p.filter-applied:nth-child(1) .filter-applied__label')); + $this->assertEquals('Commerce/Advertising', $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->clickWithWait('[aria-label="First page"]'); $this->assertProjectsVisible([ @@ -724,9 +724,9 @@ class ProjectBrowserUiTest extends WebDriverTestBase { 'Cream cheese on a bagel', ]); - $this->assertEquals('Active', $this->getElementText('p.filter-applied:nth-child(2) .filter-applied__label')); - $this->assertEquals('Commerce/Advertising', $this->getElementText('p.filter-applied:nth-child(3) .filter-applied__label')); - $this->assertEquals('Media', $this->getElementText('p.filter-applied:nth-child(4) .filter-applied__label')); + $this->assertEquals('Active', $this->getElementText('p.filter-applied:nth-child(1) .filter-applied__label')); + $this->assertEquals('Commerce/Advertising', $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')); } /** @@ -741,9 +741,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(2) .filter-applied__label')); + $this->assertEquals('Maintained', $this->getElementText('p.filter-applied:nth-child(1) .filter-applied__label')); // 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(3) .filter-applied__label')); + $this->assertEquals('Covered by a security policy', $this->getElementText('p.filter-applied:nth-child(2) .filter-applied__label')); $this->assertTrue($assert_session->waitForText('9 Results')); } @@ -765,7 +765,7 @@ class ProjectBrowserUiTest extends WebDriverTestBase { $tab_count = $page->findAll('css', '.project-browser__plugin-tabs button'); $this->assertCount(2, $tab_count); // Get result count for first tab. - $this->assertEquals('9 Results Sorted by Active installs', $this->getElementText('#output')); + $this->assertEquals('9 Results', $this->getElementText('.search-results')); // Apply filters in drupalorg_mockapi(first tab). $assert_session->waitForElement('css', '.views-exposed-form__item input[type="checkbox"]'); @@ -793,16 +793,16 @@ class ProjectBrowserUiTest extends WebDriverTestBase { $this->svelteInitHelper('css', '.filter__checkbox'); $assert_session->elementsCount('css', '.filter__checkbox', 20); $assert_session->waitForElementVisible('css', '#project-browser .project'); - $this->assertNotEquals('9 Results Sorted by Active installs', $this->getElementText('.search__results-count #output')); + $this->assertNotEquals('9 Results Sorted by Active installs', $this->getElementText('.search-results')); $assert_session->waitForElementVisible('css', '#project-browser .project'); - $result_count_text = $page->find('css', '.search__results-count #output')->getText(); + $result_count_text = $page->find('css', '.search-results')->getText(); $this->assertNotEquals('9 Results Sorted by Active installs', $result_count_text); // Apply the second module category filter. $second_category_filter_selector = '#project-browser > div.project-browser__container > .project-browser__aside > div > form > section > details > fieldset > label:nth-child(3)'; $this->clickWithWait("$second_category_filter_selector"); // Save the filter applied in second tab. - $applied_filter = $this->getElementText('p.filter-applied:nth-child(2) .filter-applied__label'); + $applied_filter = $this->getElementText('p.filter-applied:nth-child(1) .filter-applied__label'); // Save the number of results. $results_before = count($page->findAll('css', '#project-browser .project.list')); @@ -810,9 +810,9 @@ class ProjectBrowserUiTest extends WebDriverTestBase { $page->pressButton('drupalorg_mockapi'); // Assert that the filters persist. $this->assertTrue($assert_session->waitForText('4 Results')); - $first_filter_element = $page->find('css', 'p.filter-applied:nth-child(2)'); + $first_filter_element = $page->find('css', 'p.filter-applied:nth-child(1)'); $this->assertEquals('E-commerce', $first_filter_element->find('css', '.filter-applied__label')->getText()); - $second_filter_element = $page->find('css', 'p.filter-applied:nth-child(3)'); + $second_filter_element = $page->find('css', 'p.filter-applied:nth-child(2)'); $this->assertEquals('Media', $second_filter_element->find('css', '.filter-applied__label')->getText()); $this->assertProjectsVisible([ 'Tooth Fairy', @@ -824,7 +824,7 @@ class ProjectBrowserUiTest extends WebDriverTestBase { // Again switch to second tab. $page->pressButton('random_data'); // Assert that the filters persist. - $this->assertEquals($applied_filter, $this->getElementText('p.filter-applied:nth-child(2) .filter-applied__label')); + $this->assertEquals($applied_filter, $this->getElementText('p.filter-applied:nth-child(1) .filter-applied__label')); // Assert that the number of results is the same. $results_after = count($page->findAll('css', '#project-browser .project.list'));