From e8fd07d7e4f49e82f64abbe2fe83b5c3396e9044 Mon Sep 17 00:00:00 2001
From: Tim Plunkett <git@plnktt.com>
Date: Mon, 13 May 2024 17:18:20 -0400
Subject: [PATCH] Issue #3446092 by bnjmnm, phenaproxima: "View Commands"
 button assumes project is a DO module (and that a project has commands)

---
 .../ProjectBrowserSourceExample.php           |  41 ++++
 project_browser.libraries.yml                 |   1 +
 src/ProjectBrowser/Project.php                |  25 +++
 sveltejs/css/claro.css                        |   1 +
 sveltejs/public/build/bundle.js               | Bin 338969 -> 340649 bytes
 sveltejs/public/build/bundle.js.map           | Bin 311285 -> 311827 bytes
 sveltejs/src/Project/ActionButton.svelte      |   2 +-
 sveltejs/src/ProjectBrowser.svelte            |   2 +-
 sveltejs/src/popup.js                         | 209 ++++++++++--------
 .../project_browser_test.info.yml             |   2 +-
 .../ProjectBrowserUiTest.php                  |  16 +-
 11 files changed, 195 insertions(+), 104 deletions(-)

diff --git a/modules/project_browser_source_example/src/Plugin/ProjectBrowserSource/ProjectBrowserSourceExample.php b/modules/project_browser_source_example/src/Plugin/ProjectBrowserSource/ProjectBrowserSourceExample.php
index 75b1be393..c003e7b28 100644
--- a/modules/project_browser_source_example/src/Plugin/ProjectBrowserSource/ProjectBrowserSourceExample.php
+++ b/modules/project_browser_source_example/src/Plugin/ProjectBrowserSource/ProjectBrowserSourceExample.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\project_browser_source_example\Plugin\ProjectBrowserSource;
 
+use Drupal\Core\Extension\ModuleExtensionList;
 use Drupal\project_browser\Plugin\ProjectBrowserSourceBase;
 use Drupal\project_browser\ProjectBrowser\Project;
 use Drupal\project_browser\ProjectBrowser\ProjectsResultsPage;
@@ -30,12 +31,15 @@ class ProjectBrowserSourceExample extends ProjectBrowserSourceBase {
    *   The plugin implementation definition.
    * @param \Symfony\Component\HttpFoundation\RequestStack $requestStack
    *   The request from the browser.
+   * @param \Drupal\Core\Extension\ModuleExtensionList $moduleExtensionList
+   *   The module extension list.
    */
   public function __construct(
     array $configuration,
     $plugin_id,
     $plugin_definition,
     protected readonly RequestStack $requestStack,
+    protected ModuleExtensionList $moduleExtensionList,
   ) {
     parent::__construct($configuration, $plugin_id, $plugin_definition);
   }
@@ -49,6 +53,7 @@ class ProjectBrowserSourceExample extends ProjectBrowserSourceBase {
       $plugin_id,
       $plugin_definition,
       $container->get('request_stack'),
+      $container->get('extension.list.module'),
     );
   }
 
@@ -155,6 +160,42 @@ class ProjectBrowserSourceExample extends ProjectBrowserSourceBase {
         // Images: Array of images using the same structure as $logo, above.
         images: [],
       );
+      // @phpstan-ignore-next-line
+      $pb_path = $this->moduleExtensionList->getPath('project_browser');
+      $projects[] = new Project(
+        id: $project_from_source['identifier'] . '2',
+        logo: $logo,
+        // Maybe the source won't have all fields, but we still need to
+        // populate the values of all the properties.
+        isCompatible: TRUE,
+        isMaintained: TRUE,
+        isCovered: TRUE,
+        isActive: TRUE,
+        starUserCount: 0,
+        projectUsageTotal: 0,
+        machineName: $project_from_source['unique_name'] . '2',
+        body: [
+          'summary' => $project_from_source['short_description'] . ' (different commands)',
+          'value' => $project_from_source['long_description'] . ' (different commands)',
+        ],
+        title: 'A project with different commands',
+        // Status: 1 enabled / 0 disabled.
+        status: 1,
+        changed: $project_from_source['updated_at'],
+        created: $project_from_source['created_at'],
+        author: $author,
+        composerNamespace: $project_from_source['composer_namespace'],
+        categories: $categories,
+        // Images: Array of images using the same structure as $logo, above.
+        images: [],
+        type: 'different-commands',
+        commands: "<b>Steps to doing this thing</b>
+                <p>You can do it!</p>
+                <div class=\"command-box\">
+                  <input id=\"{$project_from_source['identifier']}-download-command\" value=\"composer require {$project_from_source['unique_name'] }\" readonly=\"\">
+                  <button data-copy-command><img src=\"/{$pb_path}/images/copy-icon.svg\" alt=\"Copy steps for {$project_from_source['identifier']}\"/></button> 
+                </div>",
+      );
     }
 
     // Return one page of results. The first parameter is the total number of
diff --git a/project_browser.libraries.yml b/project_browser.libraries.yml
index 351181ad8..af1f1f076 100644
--- a/project_browser.libraries.yml
+++ b/project_browser.libraries.yml
@@ -12,6 +12,7 @@ svelte:
     - core/drupal.debounce
     - core/drupal.dialog
     - core/drupal.announce
+    - core/once
     - project_browser/project_browser
 
 project_browser:
diff --git a/src/ProjectBrowser/Project.php b/src/ProjectBrowser/Project.php
index 49ba0ca3f..74df556f3 100644
--- a/src/ProjectBrowser/Project.php
+++ b/src/ProjectBrowser/Project.php
@@ -4,6 +4,7 @@ namespace Drupal\project_browser\ProjectBrowser;
 
 use Drupal\Component\Utility\Html;
 use Drupal\Component\Utility\Unicode;
+use Drupal\Component\Utility\Xss;
 
 /**
  * Defines a single Project.
@@ -53,6 +54,26 @@ class Project implements \JsonSerializable {
    *   Images of the project.
    * @param array $warnings
    *   Warnings for the project.
+   * @param string $type
+   *   The project type. Defaults to 'module:drupalorg' to indicate modules from
+   *   D.O., but may be changed to anything else that could helpfully identify
+   *   a project type.
+   * @param string|bool $commands
+   *   When FALSE, the project browser UI will not provide a "View Commands"
+   *   button for the project UNLESS the type 'module:drupalorg', in which case
+   *   it displays Svelte-generated install instructions.
+   *   When it is a string and NOT 'module:drupalorg', that string will become
+   *   the contents of the "View Commands" popup.
+   *   To include a paste-able command that includes a copy button, use this
+   *   markup structure:
+   *   @code
+   *   <div class="command-box">
+   *     <input value="THE_COMMAND_TO_BE_COPIED" readonly="" />
+   *     <button data-copy-command>
+   *       <img src="/PATH_TO_PROJECT_BROWSER/images/copy-icon.svg\" alt="ALT TEXT"/>
+   *     </button>
+   *   </div>
+   *  @endcode
    */
   public function __construct(
     public string $id,
@@ -75,6 +96,8 @@ class Project implements \JsonSerializable {
     public array $categories = [],
     public array $images = [],
     public array $warnings = [],
+    public string $type = 'module:drupalorg',
+    public string|bool $commands = FALSE,
   ) {
     $this->setSummary($body);
   }
@@ -135,6 +158,8 @@ class Project implements \JsonSerializable {
       'changed' => $this->changed,
       'created' => $this->created,
       'selector_id' => $this->getSelectorId(),
+      'type' => $this->type,
+      'commands' => Xss::filter($this->commands, [...Xss::getAdminTagList(), 'input', 'button']),
     ];
   }
 
diff --git a/sveltejs/css/claro.css b/sveltejs/css/claro.css
index d6bca4dd0..0adf3d560 100644
--- a/sveltejs/css/claro.css
+++ b/sveltejs/css/claro.css
@@ -32,6 +32,7 @@
   padding-inline: 1rem 0.25rem;
 }
 .project-browser-popup .copied-action {
+  position: absolute;
   z-index: 1;
   right: -18px;
   width: fit-content;
diff --git a/sveltejs/public/build/bundle.js b/sveltejs/public/build/bundle.js
index cdae5fdb3a9fb43b536867f2c4fcefbfd8fb3561..a6faced07b6d5205aa1ed7267371cc23a7871558 100644
GIT binary patch
delta 3230
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

delta 2522
zcmZuzdvH|c6`${XyO0fpJi$O<xhyMt*V)~K1|o<XkOY<Yv>MxzEbOv-lWe=$yYAgh
zOfoxZEQk_fAR!#W4xv^ZgE}Kv)2}*;kLgs$X@@u($4;vyQtd;g#6LRY*w*Q}`$_`r
zANTIP-|zg+`<#3Ji?WeV%6|3=TULWxCZP}~NBMR%H?UH41$Z$!Ugu>PImZonN#jcd
z{Tnml#(m5s?tPXY=jdu;GJZG4-T1E}HXlct*h1l~VN*qHgDAX@J<8cSq`Ou8dI#Ij
zS&dk{hxr)PROxS5i{xJRb0bbSu@X_$z)VJ5-%Qjk``Eh-v6ZMFYGP;bnKmYgA`iRW
zgk2rXA$-lO25?P+$>Pmc_JslekYpt|{U^gBOs)qPzUW~U_!1b(@f(e;z%OrS?cz6K
z_D&x13FgEz6Que54%TEWYjsHesN#z$2b!b4mNq3Ev$o=vLr^C6Bv_Am_=iv;{*q+Y
zby!di4pBM5@>f>0IPxX>nM|e|Wn099*BM3aw-zYDP4^fI@Wr=y;rL;A{5G6&fQ%F4
zU_keOc$wH;4yp-l)li9Ft%5(|V4OrQ4e<iJc9|E5X$yR9!Z*7>7A7Z{=b_^e*u<V{
zsNageJ4a9o*GBPa96mST#<2{u_^J!;G2u^Npyvk<K|@c<PQMz~Vv=8tbk?bDZN6|o
za!azmE#QzMQS~Q^KW3NQwUX4G0!swc;Vnw6jwH5)6g?=KE|P*}QMbnFFfHEP9DF1N
zUK&vSab4c^a9oLYKA?n1n;Mn9tMREfU}cZf+MPxxy!K5Q^;u${KNeKOMA&GnS3ANX
z)fcciB-?f}&KC;VJg#8aABqPQO?SmEZI<ZfimAKQ4kcRW(-boIF3A>5!<~U>Tx+$>
z=!hz@c(f-hHRiNRH@GZ&t=(xG5>9_QZeDvj@p|%ahEsrpN`PF-NX<rjGHMc83sOPu
zks8xNnY>UfQpkUx_?2KJ<{{T_NP<j3$|XrxmXUL5sle*TY11kzrF+$=)TSN`h7>8F
zv<Ll)=DJCgQ!U_;BVfggN5H&ljxDZsUns8R7*mF!?Fq2p`%gfHGRshx7V8Wtu2|F;
z)`FRw+0=;7AB=TMRjz7nR@iSA%L3h!tttmhIHa{zRaMw}6zZ{d6z1W9KCt2A`sHFD
z^y0_>EJQH`%h7v`Oxk?{*5LEU;4E(Mr^mK_`0>I-Hdo{We$o%sSnwpA=latdPr^Q2
zIzXMbV*qxi$(|d43SH2GUk<>JbnazyDMH7I;;G}%na1C70#@mIpS}fjY8Xm!?I4)(
zZ^KZ4`v+kub`HX;Y00fapxr{~>JZ$4g(tzAM)jNo^%kfU^%Vpot{aBMxN{hGIcFtF
z@7#16se0<8@rW<vipjRRRB5iV>CTT1!+pA4n^8Umb=am~UOokD@!zLl8>bq<6G<pF
z1OskMs!2Cd_0KJAi{uaaG|g?v9Hi^rQfuu_zewC&83?x5dP&)4q$p8wm4dVxy$II%
zdQ(!KG37`SDLncym+1UC1k2oWa<n8T{x!myyo`80E<X)-V#8@zja{eV)fsb3oPo@=
z$abv-FDGHC*f|2n*bJ`NnH>$8=C<I+XW-izz->7|YIfw<1hL&uM9q?%GOfp+o1Bu=
z*l5eD*N0VZaCsaw#xe@$OtD$-Gc!sTOARZJ$JMMxxB2|7vfLP?X`M0DGj|SKsO2f#
zH%(Y;cQBA}9*l*(cKrQeSUwc(FI`Cq$}RypLpQSpHsufXHZ4KVQCPt8vFa==K|Tso
z)<Dn~Qd{cuoKx~LJHvS}s&;5f)EQAD@rVse&%v*7#RbY&<p?Xr-S6>5_}{a%_bYw@
z<~$r5r$y!)gM5zl6ST+%Uw}RXc6>nVwekXV8nOLDTCX3CLu46Fe?)Cgn=Vgi6JTKY
z^dwobWeOIE(;vWR48Q#pT;iDzVHd;5720&=lhB7R-_1+J_#|x0!?)|XOUR$W{eW>l
z|DO2e6|geA_CIoK9rn6;Ef!s+k+QypFGl<4a2HP7jCK4#wTzV(-jCPw4J+}HN9nWY
z`c>G9mlhc2iObjE&rH<(6N(qH8Vs4Zov6$g;H3EO8+fG-9lbpB{jqZj!eWb?_s&Pt
zVY1_TJ%7xMe?Q7)an{cVR^SFCC@t?|vA37M#&K7YP<I~Y7mc`olu$)|TwaLgb7{gt
z^fa)1;Ya=tj^)o1v@6N~3i$CDm+`StzE|ux&tJcS-!~e|^Joj`%hoP795p;V_J8;#
BM%(}Z

diff --git a/sveltejs/public/build/bundle.js.map b/sveltejs/public/build/bundle.js.map
index bf947ccf70fa162466436c8fb9b1add946174626..e9d7a93e4517405cb95e552ee052a1f0303bbe05 100644
GIT binary patch
delta 4672
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

delta 4313
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

diff --git a/sveltejs/src/Project/ActionButton.svelte b/sveltejs/src/Project/ActionButton.svelte
index b71951961..e92b4627c 100644
--- a/sveltejs/src/Project/ActionButton.svelte
+++ b/sveltejs/src/Project/ActionButton.svelte
@@ -224,7 +224,7 @@
             {showStatus}
           />
         {/if}
-      {:else}
+      {:else if project.type === 'module:drupalorg' || project.commands}
         <ProjectButtonBase
           click={() => openPopup(getCommandsPopupMessage(project), project)}
           >{Drupal.t('View Commands')}
diff --git a/sveltejs/src/ProjectBrowser.svelte b/sveltejs/src/ProjectBrowser.svelte
index aeb73acc2..5de0db249 100644
--- a/sveltejs/src/ProjectBrowser.svelte
+++ b/sveltejs/src/ProjectBrowser.svelte
@@ -176,7 +176,7 @@
     }
 
     await load($page);
-    const focus = document.getElementById(element);
+    const focus = element ? document.getElementById(element) : false;
     if (focus) {
       focus.focus();
       $focusedElement = '';
diff --git a/sveltejs/src/popup.js b/sveltejs/src/popup.js
index a327a7c12..c6d80261a 100644
--- a/sveltejs/src/popup.js
+++ b/sveltejs/src/popup.js
@@ -1,96 +1,117 @@
 import { FULL_MODULE_PATH, ORIGIN_URL } from './constants';
 // cspell:ignore dont
+const { once, Drupal } = window;
 
-export const copyCommand = (cmd, project) =>  {
-  const getCopyElements = () => {
-    const getCopyElement = (suffix) => document.querySelector(`#${project.project_machine_name}-${suffix}`)
-    const action = ['Download', 'Install'].includes(cmd) ? cmd.toLowerCase() : 'install-drush';
-    return [
-      getCopyElement(`${action}-command`),
-      getCopyElement(`copied-${action}`),
-    ]
-  }
-  const [copiedCommand, copyReceipt] = getCopyElements();
-
-  copiedCommand.select();
-  // For mobile devices.
-  copiedCommand.setSelectionRange(0, 99999);
-  navigator.clipboard.writeText(copiedCommand.value);
-  copyReceipt.style.opacity = '1';
+/**
+ * Finds [data-copy-command] buttons and adds copy functionality to them.
+ */
+const enableCopyButtons = () => {
   setTimeout(() => {
-    copyReceipt.style.transition = 'opacity 0.3s';
-    copyReceipt.style.opacity = '0';
-  }, 1000);
-};
+    once('copyButton', '[data-copy-command]').forEach((copyButton) => {
+      // If clipboard is not supported (likely due to non-https), then hide the
+      // button and do not bother with event listeners
+      if (!navigator.clipboard) {
+        // copyButton.hidden = true;
+        // return;
+      }
+      copyButton.addEventListener('click', (e) => {
+        // The copy button must be contained in a div
+        const container = e.target.closest('div');
+        // The only <input> within the parent dive should have its value set
+        // to the command that should be copied.
+        const input = container.querySelector('input');
+
+        // Make the input value the selected text
+        input.select()
+        input.setSelectionRange(0, 99999);
+        navigator.clipboard.writeText(input.value);
+        Drupal.announce(Drupal.t('Copied text to clipboard'));
+
+        // Create a "receipt" that will visually show the text has been copied.
+        const receipt = document.createElement('div')
+        receipt.textContent = Drupal.t('Copied')
+        receipt.classList.add('copied-action')
+        receipt.style.opacity = '1';
+        input.insertAdjacentElement('afterend', receipt)
+        // eslint-disable-next-line max-nested-callbacks
+        setTimeout(() => {
+          // Remove the receipt after 1 second.
+          receipt.remove()
+        }, 1000);
+      })
+    })
+  })
+}
 
 export const getCommandsPopupMessage = (project) => {
-  const download = Drupal.t('Download');
-  const composerText = Drupal.t(
-    'The !use_composer_open recommended way!close to download any Drupal module is with !get_composer_open Composer!close.',
-    {
-      '!close': '</a>',
-      '!use_composer_open':
-        '<a href="https://www.drupal.org/docs/develop/using-composer/using-composer-to-install-drupal-and-manage-dependencies#managing-contributed" target="_blank" rel="noreferrer noopener">',
-      '!get_composer_open':
-        '<a href="https://getcomposer.org/" target="_blank" rel="noopener noreferrer">',
-    },
-  );
-  const composerExistsText = Drupal.t(
-    "If you already manage your Drupal application dependencies with Composer, run the following from the command line in your application's Composer root directory",
-  );
-  const infoText = Drupal.t('This will download the module to your codebase.');
-  const composerDontWorkText = Drupal.t(
-    "Didn't work? !learn_open Learn how to troubleshoot Composer!close",
-    {
-      '!learn_open':
-        '<a href="https://getcomposer.org/doc/articles/troubleshooting.md" target="_blank" rel="noopener noreferrer">',
-      '!close': '</a>',
-    },
-  );
-  const downloadModuleText = Drupal.t(
-    'If you cannot use Composer, you may !dl_manually_open download the module manually through your browser!close',
-    {
-      '!dl_manually_open':
-        '<a href="https://www.drupal.org/docs/user_guide/en/extend-module-install.html#s-using-the-administrative-interface" target="_blank" rel="noreferrer">',
-      '!close': '</a>',
-    },
-  );
-  const install = Drupal.t('Install');
-  const installText = Drupal.t(
-    'Go to the !module_page_open Extend page!close (admin/modules), check the box next to each module you wish to enable, then click the Install button at the bottom of the page.',
-    {
-      '!module_page_open': `<a href="${ORIGIN_URL}/admin/modules" target="_blank" rel="noopener noreferrer">`,
-      '!close': '</a>',
-    },
-  );
-  const drushText = Drupal.t(
-    'Alternatively, you can use !drush_openDrush!close to install it via the command line',
-    {
-      '!drush_open': '<a href="https://www.drush.org/latest/" target="_blank" rel="noopener noreferrer">',
-      '!close': '</a>',
-    },
-  );
-  const installDrush = Drupal.t(
-    'If Drush is not installed, this will add the tool to your codebase',
-  );
-  const copied = Drupal.t('Copied!');
-  const downloadAlt = Drupal.t('Copy the download command');
-  const installAlt = Drupal.t('Copy the install command');
-  const drushAlt = Drupal.t('Copy the install Drush command');
-  const copyIcon = `${FULL_MODULE_PATH}/images/copy-icon.svg`;
-  const makeButton = (altText, action) => `<button id="${action}-btn"><img src="${copyIcon}" alt="${altText}"/></button>
-                <div id="${project.project_machine_name}-copied-${action}" class="copied-action">${copied}</div>`
-  const downloadCopyButton = navigator.clipboard ? makeButton(downloadAlt, 'download') : '';
-  const installCopyButton = navigator.clipboard  ? makeButton(installAlt, 'install') : '';
-  const installDrushCopyButton = navigator.clipboard ? makeButton(drushAlt, 'install-drush') : '';
+  // @todo move the message provided in this condition to the 'commands'
+  // property of the project definition.
+  if (project.type === 'module:drupalorg') {
+    const download = Drupal.t('Download');
+    const composerText = Drupal.t(
+      'The !use_composer_open recommended way!close to download any Drupal module is with !get_composer_open Composer!close.',
+      {
+        '!close': '</a>',
+        '!use_composer_open':
+          '<a href="https://www.drupal.org/docs/develop/using-composer/using-composer-to-install-drupal-and-manage-dependencies#managing-contributed" target="_blank" rel="noreferrer noopener">',
+        '!get_composer_open':
+          '<a href="https://getcomposer.org/" target="_blank" rel="noopener noreferrer">',
+      },
+    );
+    const composerExistsText = Drupal.t(
+      "If you already manage your Drupal application dependencies with Composer, run the following from the command line in your application's Composer root directory",
+    );
+    const infoText = Drupal.t('This will download the module to your codebase.');
+    const composerDontWorkText = Drupal.t(
+      "Didn't work? !learn_open Learn how to troubleshoot Composer!close",
+      {
+        '!learn_open':
+          '<a href="https://getcomposer.org/doc/articles/troubleshooting.md" target="_blank" rel="noopener noreferrer">',
+        '!close': '</a>',
+      },
+    );
+    const downloadModuleText = Drupal.t(
+      'If you cannot use Composer, you may !dl_manually_open download the module manually through your browser!close',
+      {
+        '!dl_manually_open':
+          '<a href="https://www.drupal.org/docs/user_guide/en/extend-module-install.html#s-using-the-administrative-interface" target="_blank" rel="noreferrer">',
+        '!close': '</a>',
+      },
+    );
+    const install = Drupal.t('Install');
+    const installText = Drupal.t(
+      'Go to the !module_page_open Extend page!close (admin/modules), check the box next to each module you wish to enable, then click the Install button at the bottom of the page.',
+      {
+        '!module_page_open': `<a href="${ORIGIN_URL}/admin/modules" target="_blank" rel="noopener noreferrer">`,
+        '!close': '</a>',
+      },
+    );
+    const drushText = Drupal.t(
+      'Alternatively, you can use !drush_openDrush!close to install it via the command line',
+      {
+        '!drush_open': '<a href="https://www.drush.org/latest/" target="_blank" rel="noopener noreferrer">',
+        '!close': '</a>',
+      },
+    );
+    const installDrush = Drupal.t(
+      'If Drush is not installed, this will add the tool to your codebase',
+    );
+    const downloadAlt = Drupal.t('Copy the download command');
+    const installAlt = Drupal.t('Copy the install command');
+    const drushAlt = Drupal.t('Copy the install Drush command');
+    const copyIcon = `${FULL_MODULE_PATH}/images/copy-icon.svg`;
+    const makeButton = (altText, action) => `<button data-copy-command id="${action}-btn"><img src="${copyIcon}" alt="${altText}"/></button>`
+    const downloadCopyButton =  makeButton(downloadAlt, 'download');
+    const installCopyButton = makeButton(installAlt, 'install');
+    const installDrushCopyButton = makeButton(drushAlt, 'install-drush');
 
-  const div = document.createElement('div');
-  div.classList.add('window');
-  div.innerHTML = `<h3>1. ${download}</h3>
+    const div = document.createElement('div');
+    div.classList.add('window');
+    div.innerHTML = `<h3>1. ${download}</h3>
               <p>${composerText}</p>
               <p>${composerExistsText}:</p>
               <div class="command-box">
-                <input id="${project.project_machine_name}-download-command" value="composer require ${project.composer_namespace}" readonly/>
+                <input value="composer require ${project.composer_namespace}" readonly/>
                 ${downloadCopyButton}
               </div>
 
@@ -101,14 +122,14 @@ export const getCommandsPopupMessage = (project) => {
               <p>${installText}</p>
               <p>${drushText}:</p>
               <div class="command-box">
-                <input id="${project.project_machine_name}-install-command" value="drush pm:install ${project.project_machine_name}" readonly/>
+                <input value="drush pm:install ${project.project_machine_name}" readonly/>
                 ${installCopyButton}
               </div>
               </div>
 
               <p>${installDrush}:</p>
               <div class="command-box">
-                <input id="${project.project_machine_name}-install-drush-command" value="composer require drush/drush" readonly/>
+                <input value="composer require drush/drush" readonly/>
                 ${installDrushCopyButton}
               </div>
               <style>
@@ -118,21 +139,23 @@ export const getCommandsPopupMessage = (project) => {
                   border: 1px solid;
                 }
               </style>`;
-  if (navigator.clipboard) {
-    [['download', 'Download'], ['install', 'Install'], ['install-drush', 'Drush']].forEach(([id, command]) => {
-      div.querySelector(`#${id}-btn`).addEventListener('click', () => {
-        copyCommand(command, project);
-      });
-    });
+    enableCopyButtons();
+    return div;
+  }
+  if (project.commands) {
+    const div = document.createElement('div');
+    div.innerHTML = project.commands;
+    enableCopyButtons();
+    return div;
   }
-  return div;
+
 };
 
 export const openPopup = (getMessage, project) => {
   const message = typeof getMessage === 'function' ? getMessage() : getMessage;
   const popupModal = Drupal.dialog(message, {
     title: project.title,
-    dialogClass: 'project-browser-popup',
+    classes: {'ui-dialog': 'project-browser-popup'},
     width: '50rem',
   });
   popupModal.showModal();
diff --git a/tests/modules/project_browser_test/project_browser_test.info.yml b/tests/modules/project_browser_test/project_browser_test.info.yml
index ccb21531e..bdeba4dcb 100644
--- a/tests/modules/project_browser_test/project_browser_test.info.yml
+++ b/tests/modules/project_browser_test/project_browser_test.info.yml
@@ -1,6 +1,6 @@
 name: Project Browser test
 type: module
 description: 'Support module for testing Project Browser.'
-core_version_requirement: ^9 || ^10
+package: Testing
 dependencies:
   - project_browser:project_browser
diff --git a/tests/src/FunctionalJavascript/ProjectBrowserUiTest.php b/tests/src/FunctionalJavascript/ProjectBrowserUiTest.php
index ba9a61eb3..174ef53b3 100644
--- a/tests/src/FunctionalJavascript/ProjectBrowserUiTest.php
+++ b/tests/src/FunctionalJavascript/ProjectBrowserUiTest.php
@@ -200,18 +200,18 @@ class ProjectBrowserUiTest extends WebDriverTestBase {
     $this->getSession()->executeScript('navigator.clipboard = true');
     $this->assertTrue($assert_session->waitForText('By Hel Vetica'));
     $this->clickWithWait('#project-browser .project__action_button');
-    $allowed_html_field = $assert_session->fieldExists('helvetica-download-command');
-    $this->assertTrue($allowed_html_field->hasAttribute('readonly'));
-    $allowed_html_field = $assert_session->fieldExists('helvetica-install-command');
-    $this->assertTrue($allowed_html_field->hasAttribute('readonly'));
+    $require_command = $assert_session->waitForElement('css', 'input[value="composer require drupal/helvetica"]');
+    $this->assertTrue($require_command->hasAttribute('readonly'));
+    $install_command = $assert_session->waitForElement('css', 'input[value="drush pm:install helvetica"]');
+    $this->assertTrue($install_command->hasAttribute('readonly'));
 
     // Tests alt text for copy command image.
-    $download_command = $page->find('css', '#download-btn img');
-    $this->assertEquals('Copy the download command', $download_command->getAttribute('alt'));
+    $download_commands = $page->findAll('css', '.command-box img');
+    $this->assertEquals('Copy the download command', $download_commands[0]->getAttribute('alt'));
     $install_command = $page->find('css', '#install-btn img');
-    $this->assertEquals('Copy the install command', $install_command->getAttribute('alt'));
+    $this->assertEquals('Copy the install command', $download_commands[1]->getAttribute('alt'));
     $install_command = $page->find('css', '#install-drush-btn img');
-    $this->assertEquals('Copy the install Drush command', $install_command->getAttribute('alt'));
+    $this->assertEquals('Copy the install Drush command', $download_commands[2]->getAttribute('alt'));
   }
 
   /**
-- 
GitLab