Skip to content
Snippets Groups Projects
Commit bbf4465f authored by Chris Wells's avatar Chris Wells Committed by Tim Plunkett
Browse files

Issue #3310896 by Utkarsh_33, chrisfromredfin, tim.plunkett, bnjmnm, fjgarlin,...

Issue #3310896 by Utkarsh_33, chrisfromredfin, tim.plunkett, bnjmnm, fjgarlin, narendraR, rkoller, anmolgoyal74: Improve UI of results count
parent 39a42e02
No related branches found
No related tags found
No related merge requests found
......@@ -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,
......
File suppressed by a .gitattributes entry or the file's encoding is unsupported.
File suppressed by a .gitattributes entry or the file's encoding is unsupported.
File suppressed by a .gitattributes entry or the file's encoding is unsupported.
......@@ -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;
......
......@@ -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;
......
......@@ -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;
......@@ -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'));
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment