From ba9d24f4fad39262d25004ee8b88b13d4f4e00b4 Mon Sep 17 00:00:00 2001 From: utkarsh_33 <60460-Utkarsh_33@users.noreply.drupalcode.org> Date: Fri, 28 Feb 2025 15:14:49 +0000 Subject: [PATCH] Issue #3502666 by utkarsh_33, phenaproxima, tim.plunkett, narendrar, chrisfromredfin: Replace install/select button with a Drupal-standard drop button --- project_browser.libraries.yml | 2 + sveltejs/public/build/bundle.js | Bin 277318 -> 286950 bytes sveltejs/public/build/bundle.js.map | Bin 257830 -> 263788 bytes sveltejs/src/Project/ActionButton.svelte | 9 +- sveltejs/src/Project/DropButton.svelte | 76 ++++++++++++ .../ProjectBrowserInstallerUiTest.php | 89 +++++++++++++- tests/src/Nightwatch/Tests/keyboardTest.js | 112 ++++++++++++++++++ 7 files changed, 285 insertions(+), 3 deletions(-) create mode 100644 sveltejs/src/Project/DropButton.svelte diff --git a/project_browser.libraries.yml b/project_browser.libraries.yml index 1716ee1fd..6915d4612 100644 --- a/project_browser.libraries.yml +++ b/project_browser.libraries.yml @@ -15,6 +15,8 @@ svelte: - core/drupal.message - core/once - project_browser/project_browser + # This is included to support dropdown feature. + - core/drupal.dropbutton project_browser: css: diff --git a/sveltejs/public/build/bundle.js b/sveltejs/public/build/bundle.js index 86164c2f79e64779de5091ca5fa7fb7410e9da42..bd7e99307d6b01a8521602dc50258ba96ddf5183 100644 GIT binary patch delta 5373 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 PNZHtY36<tcf>QIpqxfU> delta 1830 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 WLTiV#P#pg|Mf^0LG#O92Ec!266QrX6 diff --git a/sveltejs/public/build/bundle.js.map b/sveltejs/public/build/bundle.js.map index e2cb7317713d25d6380d7e3b939502e3c0dd10e5..f331147d9ab69fc4396616004277cc0bd59f21f3 100644 GIT binary patch delta 4281 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 delta 278 zcmV+x0qOqij1Z>Y53n`^gIoi*Tmu1q%a@FI0ST8(_5l#L``7`q2$$X!0yvfz0tc6_ z;Q>OH7XmYv@ZkYFmz-<?AeX!10cMw__5o#;egX=Y6!!rlmw%lB2A9jU0ZO+`<N;6w zxA^D*UUG+OTLQOgTLZ`w2YEz8ZA559w+?y(>k9!>m->YR*ak^KXhn5Hmm7!!E|&<0 z0}q!>AOk4`NkK+4ml}uzXaQ%pgNOs#2mwczUXcSZ0Y|rwkprUv2s=AFNkMEvFPGnw z1B?Q5MVH~P14NhLHUk+2S3z_~PnSWX104ZcmyAaPKesxS18D&QI7FAFmIG4(aF_X( c1J40#x5<|S+XDkmENPdKkpnQd*P8>CYA!%#K>z>% diff --git a/sveltejs/src/Project/ActionButton.svelte b/sveltejs/src/Project/ActionButton.svelte index 01f7429a5..9e7c5704e 100644 --- a/sveltejs/src/Project/ActionButton.svelte +++ b/sveltejs/src/Project/ActionButton.svelte @@ -1,19 +1,21 @@ <script> import { PACKAGE_MANAGER, MAX_SELECTIONS } from '../constants'; import { openPopup, getCommandsPopupMessage } from '../popup'; - import ProjectButtonBase from './ProjectButtonBase.svelte'; import ProjectStatusIndicator from './ProjectStatusIndicator.svelte'; - import ProjectIcon from './ProjectIcon.svelte'; import LoadingEllipsis from './LoadingEllipsis.svelte'; + import DropButton from './DropButton.svelte'; + import ProjectButtonBase from './ProjectButtonBase.svelte'; import { processInstallList, addToInstallList, installList, removeFromInstallList, } from '../InstallListProcessor'; + import ProjectIcon from './ProjectIcon.svelte'; // eslint-disable-next-line import/no-mutable-exports,import/prefer-default-export export let project; + let InstallListFull; const { Drupal } = window; const processMultipleProjects = MAX_SELECTIONS === null || MAX_SELECTIONS > 1; @@ -52,6 +54,9 @@ <ProjectStatusIndicator {project} statusText={Drupal.t('Installed')}> <ProjectIcon type="installed" /> </ProjectStatusIndicator> + {#if project.tasks.length > 0} + <DropButton tasks={project.tasks} /> + {/if} {:else} <span> {#if PACKAGE_MANAGER} diff --git a/sveltejs/src/Project/DropButton.svelte b/sveltejs/src/Project/DropButton.svelte new file mode 100644 index 000000000..16451c017 --- /dev/null +++ b/sveltejs/src/Project/DropButton.svelte @@ -0,0 +1,76 @@ +<script> + // eslint-disable-next-line import/prefer-default-export + export let tasks = []; + + // Toggle the dropdown visibility for the clicked drop button + const toggleDropdown = (event) => { + const wrapper = event.currentTarget.closest('.dropbutton-wrapper'); + const isOpen = wrapper.classList.contains('open'); + + // Close all open dropdowns first + document.querySelectorAll('.dropbutton-wrapper.open').forEach((el) => { + el.classList.remove('open'); + }); + + if (!isOpen) { + wrapper.classList.add('open'); + } + }; + + // Handle keydown for closing the dropdown with Escape + const handleKeyDown = (event) => { + // Query the DOM for getting the only opened dropbutton. + const openDropdown = document.querySelector('.dropbutton-wrapper.open'); + if (!openDropdown) return; + + // If there are no items in the dropdown, exit early + if (!openDropdown.querySelectorAll('.secondary-action a').length) return; + + const toggleButton = openDropdown.querySelector('.dropbutton__toggle'); + if (event.key === 'Escape') { + openDropdown.classList.remove('open'); + toggleButton.focus(); + } + }; + + // Close the dropdown if clicked outside + const closeDropdownOnOutsideClick = (event) => { + document.querySelectorAll('.dropbutton-wrapper.open').forEach((wrapper) => { + if (!wrapper.contains(event.target)) { + wrapper.classList.remove('open'); + } + }); + }; + document.addEventListener('click', closeDropdownOnOutsideClick); + document.addEventListener('keydown', handleKeyDown); +</script> + +<div class="dropbutton-wrapper dropbutton-multiple" data-once="dropbutton"> + <div class="dropbutton-widget"> + <ul class="dropbutton dropbutton--extrasmall dropbutton--multiple"> + <li class="dropbutton__item dropbutton-action"> + <a href={tasks[0].url} on:click={() => {}} class="pb__action_button"> + {tasks[0].text} + </a> + </li> + + {#if tasks.length > 1} + <li class="dropbutton-toggle"> + <button + type="button" + class="dropbutton__toggle" + on:click={toggleDropdown} + > + <span class="visually-hidden">List additional actions</span> + </button> + </li> + + {#each tasks.slice(1) as task} + <li class="dropbutton__item dropbutton-action secondary-action"> + <a href={task.url}>{task.text}</a> + </li> + {/each} + {/if} + </ul> + </div> +</div> diff --git a/tests/src/FunctionalJavascript/ProjectBrowserInstallerUiTest.php b/tests/src/FunctionalJavascript/ProjectBrowserInstallerUiTest.php index a0c09d22e..d0a069862 100644 --- a/tests/src/FunctionalJavascript/ProjectBrowserInstallerUiTest.php +++ b/tests/src/FunctionalJavascript/ProjectBrowserInstallerUiTest.php @@ -4,6 +4,8 @@ declare(strict_types=1); namespace Drupal\Tests\project_browser\FunctionalJavascript; +use Behat\Mink\Element\NodeElement; +use Drupal\Core\Extension\ModuleInstallerInterface; use Drupal\Core\Recipe\Recipe; use Drupal\Core\State\StateInterface; use Drupal\FunctionalJavascriptTests\WebDriverTestBase; @@ -60,7 +62,7 @@ class ProjectBrowserInstallerUiTest extends WebDriverTestBase { $this->installState = $install_state; $this->config('project_browser.admin_settings') - ->set('enabled_sources', ['project_browser_test_mock']) + ->set('enabled_sources', ['project_browser_test_mock', 'drupal_core', 'recipes']) ->set('allow_ui_install', TRUE) ->set('max_selections', 1) ->save(); @@ -383,4 +385,89 @@ class ProjectBrowserInstallerUiTest extends WebDriverTestBase { $this->assertTrue($cream_cheese->hasButton('Install Cream cheese on a bagel')); } + /** + * Tests the drop button actions for a project. + */ + public function testDropButtonActions(): void { + $this->container->get(ModuleInstallerInterface::class)->install([ + 'config_translation', + 'contact', + 'content_translation', + 'help', + ]); + $this->rebuildContainer(); + + $this->drupalGet('admin/modules/browse/recipes'); + $this->svelteInitHelper('css', '.pb-projects-list'); + + $card = $this->waitForProject('Admin theme'); + $card->pressButton('Install'); + $this->waitForProjectToBeInstalled($card); + // Now assert that the dropdown button does not appear when + // we don't have any follow-up actions. + $this->assertNull($this->assertSession()->waitForElementVisible('css', '.dropbutton .secondary-action a')); + + $this->drupalGet('admin/modules/browse/drupal_core'); + $this->svelteInitHelper('css', '.pb-project.pb-project--list'); + $this->inputSearchField('contact', TRUE); + $this->assertElementIsVisible('css', ".search__search-submit")->click(); + $card = $this->waitForProject('Contact'); + $card->pressButton('List additional actions'); + $this->assertChildElementIsVisible($card, 'css', '.dropbutton .secondary-action a'); + + $available_actions = []; + foreach ($card->findAll('css', '.dropbutton .dropbutton-action a') as $item) { + $available_actions[$item->getText()] = $item->getAttribute('href') ?? ''; + } + + // Assert expected dropdown actions exist and point to the correct places. + $this->assertStringEndsWith('/admin/structure/contact', $available_actions['Configure']); + $this->assertStringEndsWith('/admin/help/contact', $available_actions['Help']); + $this->assertStringContainsString('/project-browser/uninstall/contact', $available_actions['Uninstall']); + + // Ensure that dropdown menus are mutually exclusive. + $this->inputSearchField('translation', TRUE); + $this->assertElementIsVisible('css', ".search__search-submit")->click(); + $project1 = $this->waitForProject('Content Translation'); + $project2 = $this->waitForProject('Configuration Translation'); + + // Ensure that an open dropdown closes when you click outside of it. + $project1->pressButton('List additional actions'); + $this->assertChildElementIsVisible($project1, 'css', '.dropbutton .secondary-action a'); + $project2->click(); + $this->assertFalse($project1->find('css', '.dropbutton .secondary-action')?->isVisible()); + + // Ensure that there can only be one open dropdown at a time. + $project2->pressButton('List additional actions'); + $this->assertChildElementIsVisible($project2, 'css', '.dropbutton .secondary-action a'); + $project1->pressButton('List additional actions'); + $this->assertChildElementIsVisible($project1, 'css', '.dropbutton .secondary-action a'); + $this->assertFalse($project2->find('css', '.dropbutton .secondary-action')?->isVisible()); + + // Ensure that we can close an open dropdown by clicking the button again. + $project1->pressButton('List additional actions'); + $this->assertFalse($project1->find('css', '.dropbutton .secondary-action')?->isVisible()); + } + + /** + * Waits for a child of a particular element, to be visible. + * + * @param \Behat\Mink\Element\NodeElement $parent + * An element that (presumably) contains children. + * @param string $selector + * The selector (e.g., `css`, `xpath`, etc.) to use to find a child element. + * @param mixed $locator + * The locator to pass to the selector engine. + * @param int $timeout + * (optional) How many seconds to wait for the child element to appear. + * Defaults to 10. + */ + private function assertChildElementIsVisible(NodeElement $parent, string $selector, mixed $locator, int $timeout = 10): void { + $is_visible = $parent->waitFor( + $timeout, + fn (NodeElement $parent) => $parent->find($selector, $locator)?->isVisible(), + ); + $this->assertTrue($is_visible); + } + } diff --git a/tests/src/Nightwatch/Tests/keyboardTest.js b/tests/src/Nightwatch/Tests/keyboardTest.js index b8d83d0c3..ad2daa59b 100644 --- a/tests/src/Nightwatch/Tests/keyboardTest.js +++ b/tests/src/Nightwatch/Tests/keyboardTest.js @@ -1,6 +1,8 @@ const delayInMilliseconds = 100; const filterKeywordSearch = '#pb-text'; const filterDropdownSelector = '.pb-filter__multi-dropdown'; +const dropButtonSelector = 'button.dropbutton__toggle'; +const dropButtonItemSelector = 'ul.dropbutton li.secondary-action a'; module.exports = { '@tags': ['project_browser'], @@ -36,6 +38,52 @@ module.exports = { return this.actions().sendKeys(browser.Keys.ESCAPE); } browser.drupalLoginAsAdmin(() => { + // We are enabling some modules in order to test the follow-up + // actions for some already installed modules in drupal core. + browser + .drupalRelativeURL('/admin/modules') + .click('[name="modules[package_manager][enable]"]') + .click('[name="modules[contact][enable]"]') + .click('[name="modules[help][enable]"]') + .submitForm('input[type="submit"]') + .waitForElementVisible( + '.system-modules-confirm-form input[value="Continue"]', + ) + .submitForm('input[value="Continue"]') + .waitForElementVisible('.system-modules', 10000); + browser + .drupalRelativeURL('/admin/config/development/project_browser') + .waitForElementVisible( + '[data-drupal-selector="edit-allow-ui-install"]', + delayInMilliseconds, + ) + .click('[data-drupal-selector="edit-allow-ui-install"]') + + // Wait for the select element and enable it + .waitForElementVisible( + '[data-drupal-selector="edit-enabled-sources-drupal-core-status"]', + delayInMilliseconds, + ) + .execute( + (selector) => { + document.querySelector(selector).removeAttribute('disabled'); + }, + ['[data-drupal-selector="edit-enabled-sources-drupal-core-status"]'], + ) + + .click( + '[data-drupal-selector="edit-enabled-sources-drupal-core-status"]', + ) + .click( + '[data-drupal-selector="edit-enabled-sources-drupal-core-status"] option[value="enabled"]', + ) + + // Click the Save Configuration button + .waitForElementVisible( + '[data-drupal-selector="edit-submit"]', + delayInMilliseconds, + ) + .click('[data-drupal-selector="edit-submit"]'); // Open project browser. browser .drupalRelativeURL('/admin/modules/browse/project_browser_test_mock') @@ -169,6 +217,70 @@ module.exports = { 'Assert that no filter lozenge is shown.', ); + browser + .drupalRelativeURL('/admin/modules/browse/drupal_core') + .waitForElementVisible('h1', delayInMilliseconds) + .assert.textContains('h1', 'Browse projects') + .waitForElementVisible('#aaa_update_test_title'); + + browser + .setValue('#pb-text', 'contact') + .waitForElementVisible('button.search__search-submit', 5000) + .execute(() => + document.querySelector('button.search__search-submit').click(), + ) + .pause(1000) + .waitForElementVisible('#contact_title', 1000); + + // Directly focus on the security icon. + browser + .waitForElementVisible('.pb-project__status-icon-btn', 1000) + .execute( + (selector) => { + const el = document.querySelector(selector); + if (el) { + el.focus(); + } + }, + ['.pb-project__status-icon-btn'], + ); + // Navigate to maintenance icon. + browser.perform(sendTabKey).pause(1000); + // Navigate to installed button. + browser.perform(sendTabKey).pause(1000); + // Navigate to Installed button. + browser.perform(sendTabKey).pause(1000); + // Navigate to dropdown button. + browser.perform(sendTabKey).pause(1000); + assertFocus(dropButtonSelector, 'Assert dropbutton has focus.'); + + // Press space to open the dropbutton menu. + browser.perform(sendSpaceKey).pause(1000); + browser.assert.visible( + dropButtonItemSelector, + 'Assert dropbutton menu is visible.', + ); + + // Navigate to first dropbutton item using keyboard. + browser.perform(sendTabKey).pause(delayInMilliseconds); + assertFocus( + 'ul.dropbutton li.secondary-action a', + 'Assert first dropbutton item has focus.', + ); + // Navigate to second dropbutton item using keyboard. + browser.perform(sendTabKey).pause(delayInMilliseconds); + assertFocus( + 'ul.dropbutton li.secondary-action a', + 'Assert second dropbutton item has focus.', + ); + + // Press escape to close the dropbutton menu. + browser.perform(sendEscapeKey).pause(1000); + assertFocus( + dropButtonSelector, + 'Assert focus returns to dropbutton after closing.', + ); + // Close out test. browser.drupalLogAndEnd({ onlyOnError: false }); }); -- GitLab