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