diff --git a/app/assets/javascripts/pages/projects/show/index.js b/app/assets/javascripts/pages/projects/show/index.js
index 150c702f1fe942bddd8e6f27a2b62e3934702da0..adcbf1217e14464d84f9fdc5ac135f583188c454 100644
--- a/app/assets/javascripts/pages/projects/show/index.js
+++ b/app/assets/javascripts/pages/projects/show/index.js
@@ -1,3 +1,4 @@
+import Vue from 'vue';
 import { addShortcutsExtension } from '~/behaviors/shortcuts';
 import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
 import initClustersDeprecationAlert from '~/projects/clusters_deprecation_alert';
@@ -10,6 +11,7 @@ import initReadMore from '~/read_more';
 import initForksButton from '~/forks/init_forks_button';
 import initAmbiguousRefModal from '~/ref/init_ambiguous_ref_modal';
 import InitMoreActionsDropdown from '~/groups_projects/init_more_actions_dropdown';
+import CodeDropdown from '~/vue_shared/components/code_dropdown/code_dropdown.vue';
 
 // Project show page loads different overview content based on user preferences
 if (document.getElementById('js-tree-list')) {
@@ -63,3 +65,28 @@ if (document.querySelector('.js-autodevops-banner')) {
 initForksButton();
 InitMoreActionsDropdown();
 leaveByUrl('project');
+
+if (document.getElementById('empty-page')) {
+  const initCodeDropdown = () => {
+    const codeDropdownEl = document.getElementById('js-code-dropdown');
+
+    if (!codeDropdownEl) return false;
+
+    const { sshUrl, httpUrl, kerberosUrl } = codeDropdownEl.dataset;
+
+    return new Vue({
+      el: codeDropdownEl,
+      render(createElement) {
+        return createElement(CodeDropdown, {
+          props: {
+            sshUrl,
+            httpUrl,
+            kerberosUrl,
+          },
+        });
+      },
+    });
+  };
+
+  initCodeDropdown();
+}
diff --git a/app/assets/javascripts/repository/index.js b/app/assets/javascripts/repository/index.js
index afe3f7b1983ef09bb9ba805fbd7c39b329e849aa..25141ec31a8f0696d915cf758e0d9672079a3377 100644
--- a/app/assets/javascripts/repository/index.js
+++ b/app/assets/javascripts/repository/index.js
@@ -10,9 +10,9 @@ import PerformancePlugin from '~/performance/vue_performance_plugin';
 import createStore from '~/code_navigation/store';
 import RefSelector from '~/ref/components/ref_selector.vue';
 import HighlightWorker from '~/vue_shared/components/source_viewer/workers/highlight_worker?worker';
+import CodeDropdown from '~/vue_shared/components/code_dropdown/code_dropdown.vue';
 import App from './components/app.vue';
 import Breadcrumbs from './components/breadcrumbs.vue';
-import DirectoryDownloadLinks from './components/directory_download_links.vue';
 import ForkInfo from './components/fork_info.vue';
 import LastCommit from './components/last_commit.vue';
 import BlobControls from './components/blob_controls.vue';
@@ -170,6 +170,38 @@ export default function setupVueRepositoryList() {
     });
   };
 
+  const initCodeDropdown = () => {
+    const codeDropdownEl = document.getElementById('js-code-dropdown');
+
+    if (!codeDropdownEl) return false;
+
+    const {
+      sshUrl,
+      httpUrl,
+      kerberosUrl,
+      xcodeUrl,
+      directoryDownloadLinks,
+    } = codeDropdownEl.dataset;
+
+    return new Vue({
+      el: codeDropdownEl,
+      router,
+      render(createElement) {
+        return createElement(CodeDropdown, {
+          props: {
+            sshUrl,
+            httpUrl,
+            kerberosUrl,
+            xcodeUrl,
+            currentPath: this.$route.params.path,
+            directoryDownloadLinks: JSON.parse(directoryDownloadLinks),
+          },
+        });
+      },
+    });
+  };
+
+  initCodeDropdown();
   initLastCommitApp();
   initBlobControlsApp();
   initRefSwitcher();
@@ -259,30 +291,6 @@ export default function setupVueRepositoryList() {
 
   initWebIdeLink({ el: document.getElementById('js-tree-web-ide-link'), router });
 
-  const directoryDownloadLinks = document.querySelector('.js-directory-downloads');
-
-  if (directoryDownloadLinks) {
-    // eslint-disable-next-line no-new
-    new Vue({
-      el: directoryDownloadLinks,
-      router,
-      render(h) {
-        const currentPath = this.$route.params.path || '/';
-
-        if (currentPath !== '/') {
-          return h(DirectoryDownloadLinks, {
-            props: {
-              currentPath: currentPath.replace(/^\//, ''),
-              links: JSON.parse(directoryDownloadLinks.dataset.links),
-            },
-          });
-        }
-
-        return null;
-      },
-    });
-  }
-
   // eslint-disable-next-line no-new
   new Vue({
     el,
diff --git a/app/assets/javascripts/vue_shared/components/clone_dropdown/clone_dropdown_item.vue b/app/assets/javascripts/vue_shared/components/clone_dropdown/clone_dropdown_item.vue
index 6980e19733aa3170bc34c38d7536fbf1ceb64d5a..db5db7ebdfac6accbc6e78d101251bce48cbf5b8 100644
--- a/app/assets/javascripts/vue_shared/components/clone_dropdown/clone_dropdown_item.vue
+++ b/app/assets/javascripts/vue_shared/components/clone_dropdown/clone_dropdown_item.vue
@@ -23,10 +23,25 @@ export default {
       type: String,
       required: true,
     },
+    labelClass: {
+      type: String,
+      required: false,
+      default: '',
+    },
     link: {
       type: String,
       required: true,
     },
+    inputTestId: {
+      type: String,
+      required: false,
+      default: '',
+    },
+    name: {
+      type: String,
+      required: false,
+      default: null,
+    },
     testId: {
       type: String,
       required: true,
@@ -37,8 +52,20 @@ export default {
 </script>
 <template>
   <gl-disclosure-dropdown-item>
-    <gl-form-group :label="label" class="gl-px-3 gl-mb-3">
-      <gl-form-input-group :value="link" readonly select-on-click>
+    <gl-form-group
+      :label="label"
+      :label-class="labelClass"
+      :label-for="inputTestId"
+      class="gl-px-3 gl-mb-3 gl-text-left"
+    >
+      <gl-form-input-group
+        :id="inputTestId"
+        :value="link"
+        :name="name"
+        :data-testid="inputTestId"
+        readonly
+        select-on-click
+      >
         <template #append>
           <gl-button
             v-gl-tooltip.hover
diff --git a/app/assets/javascripts/vue_shared/components/code_dropdown/code_dropdown.vue b/app/assets/javascripts/vue_shared/components/code_dropdown/code_dropdown.vue
new file mode 100644
index 0000000000000000000000000000000000000000..e42c086b541d4faf2c506a3b41e6f181b4763e4a
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/code_dropdown/code_dropdown.vue
@@ -0,0 +1,213 @@
+<script>
+import { GlDisclosureDropdown, GlDisclosureDropdownGroup, GlTooltipDirective } from '@gitlab/ui';
+import { getHTTPProtocol } from '~/lib/utils/url_utility';
+import { __, sprintf } from '~/locale';
+import CloneDropdownItem from '~/vue_shared/components/clone_dropdown/clone_dropdown_item.vue';
+
+export default {
+  components: {
+    GlDisclosureDropdown,
+    GlDisclosureDropdownGroup,
+    CloneDropdownItem,
+  },
+  directives: {
+    GlTooltip: GlTooltipDirective,
+  },
+  props: {
+    sshUrl: {
+      type: String,
+      required: false,
+      default: '',
+    },
+    httpUrl: {
+      type: String,
+      required: false,
+      default: '',
+    },
+    kerberosUrl: {
+      type: String,
+      required: false,
+      default: null,
+    },
+    xcodeUrl: {
+      type: String,
+      required: false,
+      default: '',
+    },
+    currentPath: {
+      type: String,
+      required: false,
+      default: null,
+    },
+    directoryDownloadLinks: {
+      type: Array,
+      required: false,
+      default: null,
+    },
+  },
+  computed: {
+    httpLabel() {
+      const protocol = this.httpUrl ? getHTTPProtocol(this.httpUrl)?.toUpperCase() : '';
+      return sprintf(__('Clone with %{protocol}'), { protocol });
+    },
+    sshUrlEncoded() {
+      return encodeURIComponent(this.sshUrl);
+    },
+    httpUrlEncoded() {
+      return encodeURIComponent(this.httpUrl);
+    },
+    unfilteredDropdownItems() {
+      return [
+        {
+          item: {
+            text: __('Visual Studio Code (SSH)'),
+            href: `${this.vsCodeBaseUrl}${this.sshUrlEncoded}`,
+          },
+          isIncluded: Boolean(this.sshUrl),
+        },
+        {
+          item: {
+            text: __('Visual Studio Code (HTTPS)'),
+            href: `${this.vsCodeBaseUrl}${this.httpUrlEncoded}`,
+          },
+          isIncluded: Boolean(this.httpUrl),
+        },
+        {
+          item: {
+            text: __('IntelliJ IDEA (SSH)'),
+            href: `${this.jetBrainsBaseUrl}${this.sshUrlEncoded}`,
+          },
+          isIncluded: Boolean(this.sshUrl),
+        },
+        {
+          item: {
+            text: __('IntelliJ IDEA (HTTPS)'),
+            href: `${this.jetBrainsBaseUrl}${this.httpUrlEncoded}`,
+          },
+          isIncluded: Boolean(this.httpUrl),
+        },
+        {
+          item: {
+            text: __('Xcode'),
+            href: this.xcodeUrl,
+          },
+          isIncluded: Boolean(this.xcodeUrl),
+        },
+      ];
+    },
+    ideGroup() {
+      const items = [];
+
+      this.unfilteredDropdownItems.forEach(({ item, isIncluded }) => {
+        if (isIncluded) {
+          items.push(item);
+        }
+      });
+
+      return {
+        name: this.$options.i18n.openInIDE,
+        items,
+      };
+    },
+    sourceCodeGroup() {
+      const items = this.directoryDownloadLinks.map((link) => ({
+        text: link.text,
+        href: link.path,
+        extraAttrs: {
+          rel: 'nofollow',
+          download: '',
+        },
+      }));
+
+      return {
+        name: this.$options.i18n.downloadSourceCode,
+        items,
+      };
+    },
+    directoryDownloadLinksGroup() {
+      const items = this.directoryDownloadLinks.map((link) => ({
+        text: link.text,
+        href: `${link.path}?path=${this.currentPath}`,
+        extraAttrs: {
+          rel: 'nofollow',
+          download: '',
+        },
+      }));
+
+      return {
+        name: this.$options.i18n.downloadDirectory,
+        items,
+      };
+    },
+  },
+  vsCodeBaseUrl: 'vscode://vscode.git/clone?url=',
+  jetBrainsBaseUrl:
+    'jetbrains://idea/checkout/git?idea.required.plugins.id=Git4Idea&checkout.repo=',
+  i18n: {
+    defaultLabel: __('Code'),
+    cloneWithSsh: __('Clone with SSH'),
+    cloneWithKerberos: __('Clone with KRB5'),
+    openInIDE: __('Open in your IDE'),
+    downloadSourceCode: __('Download Source Code'),
+    downloadDirectory: __('Download this directory'),
+  },
+};
+</script>
+<template>
+  <gl-disclosure-dropdown
+    :toggle-text="$options.i18n.defaultLabel"
+    category="primary"
+    variant="confirm"
+    placement="right"
+    class="code-dropdown gl-text-left"
+    fluid-width
+    data-testid="code-dropdown"
+  >
+    <gl-disclosure-dropdown-group v-if="sshUrl">
+      <clone-dropdown-item
+        :label="$options.i18n.cloneWithSsh"
+        label-class="gl-font-sm! gl-pt-2!"
+        :link="sshUrl"
+        name="ssh_project_clone"
+        input-test-id="copy-ssh-url-input"
+        test-id="copy-ssh-url-button"
+      />
+    </gl-disclosure-dropdown-group>
+    <gl-disclosure-dropdown-group v-if="httpUrl">
+      <clone-dropdown-item
+        :label="httpLabel"
+        label-class="gl-font-sm! gl-pt-2!"
+        :link="httpUrl"
+        name="http_project_clone"
+        input-test-id="copy-http-url-input"
+        test-id="copy-http-url-button"
+      />
+    </gl-disclosure-dropdown-group>
+    <gl-disclosure-dropdown-group v-if="kerberosUrl">
+      <clone-dropdown-item
+        :label="$options.i18n.cloneWithKerberos"
+        label-class="gl-font-sm! gl-pt-2!"
+        :link="kerberosUrl"
+        name="kerberos_project_clone"
+        input-test-id="copy-http-url-input"
+        test-id="copy-http-url-button"
+      />
+    </gl-disclosure-dropdown-group>
+    <gl-disclosure-dropdown-group :group="ideGroup" bordered />
+    <gl-disclosure-dropdown-group v-if="directoryDownloadLinks" :group="sourceCodeGroup" bordered />
+    <gl-disclosure-dropdown-group
+      v-if="currentPath && directoryDownloadLinks"
+      :group="directoryDownloadLinksGroup"
+      bordered
+    />
+  </gl-disclosure-dropdown>
+</template>
+<style>
+/* Temporary override until we have
+   * widths available in GlDisclosureDropdown
+   */
+.code-dropdown .gl-new-dropdown-panel {
+  width: 100%;
+  max-width: 348px;
+}
+</style>
diff --git a/app/views/projects/buttons/_code.html.haml b/app/views/projects/buttons/_code.html.haml
index 9cdbe1d5f6b54d38d103b0f072c68341f1574fb1..6c894748bf04dbd4b7eb39e8f322d7e2fea15453 100644
--- a/app/views/projects/buttons/_code.html.haml
+++ b/app/views/projects/buttons/_code.html.haml
@@ -1,59 +1,12 @@
 - project = project || @project
-- dropdown_class = local_assigns.fetch(:dropdown_class, '')
-- ref = local_assigns.fetch(:ref)
+- ref = local_assigns.fetch(:ref) || ''
+- archive_prefix = ref ? "#{project.path}-#{ref.tr('/', '-')}" : ''
 
 - if can?(current_user, :download_code, @project)
   .git-clone-holder.js-git-clone-holder
-    = render Pajamas::ButtonComponent.new(variant: :confirm, button_options: { id: 'clone-dropdown', class: 'clone-dropdown-btn', data: { toggle: 'dropdown', testid: 'clone-dropdown' } }) do
-      %span.js-clone-dropdown-label
-        = _('Code')
-      = sprite_icon("chevron-down", css_class: "icon")
-    %ul.dropdown-menu.dropdown-menu-large.clone-options-dropdown{ role: 'menu', class: dropdown_class, data: { testid: 'clone-dropdown-content' } }
-      - if ssh_enabled?
-        %li.gl-dropdown-item.js-clone-links{ role: 'menuitem', class: 'gl-px-4!' }
-          %label.label-bold
-            = _('Clone with SSH')
-          .input-group.btn-group
-            = text_field_tag :ssh_project_clone, ssh_clone_url_to_repo(project), class: "js-select-on-focus form-control", readonly: true, aria: { label: _('Repository clone URL') }, data: { testid: 'ssh-clone-url-content' }
-            .input-group-append
-              = clipboard_button(target: '#ssh_project_clone', title: _("Copy URL"), category: :primary, size: :medium)
-              = render_if_exists 'projects/buttons/geo'
-      - if http_enabled?
-        %li.pt-2.gl-dropdown-item.js-clone-links{ role: 'menuitem',  class: 'gl-px-4!' }
-          %label.label-bold
-            = _('Clone with %{http_label}') % { http_label: gitlab_config.protocol.upcase }
-          .input-group.btn-group
-            = text_field_tag :http_project_clone, http_clone_url_to_repo(project), class: "js-select-on-focus form-control", readonly: true, aria: { label: _('Repository clone URL') }, data: { testid: 'http-clone-url-content' }
-            .input-group-append
-              = clipboard_button(target: '#http_project_clone', title: _("Copy URL"), category: :primary, size: :medium)
-              = render_if_exists 'projects/buttons/geo'
-        = render_if_exists 'projects/buttons/kerberos_clone_field'
-      %li.divider.mt-2{ role: 'presentation' }
-      %li.pt-2.gl-dropdown-item.js-clone-links{ role: 'menuitem' }
-        %label.label-bold{ class: 'gl-px-4!' }
-          = _('Open in your IDE')
-        - if ssh_enabled?
-          - escaped_ssh_url_to_repo = CGI.escape(ssh_clone_url_to_repo(project))
-          %a.dropdown-item.open-with-link{ href: 'vscode://vscode.git/clone?url=' + escaped_ssh_url_to_repo }
-            .gl-dropdown-item-text-wrapper
-              = _('Visual Studio Code (SSH)')
-        - if http_enabled?
-          - escaped_http_url_to_repo = CGI.escape(http_clone_url_to_repo(project))
-          %a.dropdown-item.open-with-link{ href: 'vscode://vscode.git/clone?url=' + escaped_http_url_to_repo }
-            .gl-dropdown-item-text-wrapper
-              = _('Visual Studio Code (HTTPS)')
-        - if ssh_enabled?
-          %a.dropdown-item.open-with-link{ href: 'jetbrains://idea/checkout/git?idea.required.plugins.id=Git4Idea&checkout.repo=' + escaped_ssh_url_to_repo }
-            .gl-dropdown-item-text-wrapper
-              = _('IntelliJ IDEA (SSH)')
-        - if http_enabled?
-          %a.dropdown-item.open-with-link{ href: 'jetbrains://idea/checkout/git?idea.required.plugins.id=Git4Idea&checkout.repo=' + escaped_http_url_to_repo }
-            .gl-dropdown-item-text-wrapper
-              = _('IntelliJ IDEA (HTTPS)')
-        - if show_xcode_link?(@project)
-          %a.dropdown-item.open-with-link{ href: xcode_uri_to_repo(@project) }
-            .gl-dropdown-item-text-wrapper
-              = _("Xcode")
-        - if !project.empty_repo? && can?(current_user, :download_code, project)
-          %li.divider.mt-2{ role: 'presentation' }
-          = render 'projects/buttons/download_menu_items', project: project, ref: ref
+    #js-code-dropdown{ data: {
+      ssh_url: ssh_enabled? ? ssh_clone_url_to_repo(@project) : '',
+      http_url: http_enabled? ? http_clone_url_to_repo(@project) : '',
+      xcode_url: show_xcode_link?(@project) ? xcode_uri_to_repo(@project) : '',
+      directory_download_links: directory_download_links(project, ref, archive_prefix).to_json,
+    } }
diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml
index ac3b67d6157f3b03a67a4c463263b107ba7dc6a9..e2ad6dd83f8d18be3ee8b82a10431e2a88e9c896 100644
--- a/app/views/projects/empty.html.haml
+++ b/app/views/projects/empty.html.haml
@@ -14,7 +14,7 @@
 
   .project-page-indicator.js-show-on-project-root
 
-  .project-page-layout
+  #empty-page.project-page-layout
     .project-page-layout-content.gl-mt-5
       .project-buttons.gl-mb-5{ data: { testid: 'quick-actions-container' } }
         .project-clone-holder.d-block.d-sm-none
@@ -96,7 +96,7 @@
     .project-clone-holder.d-block.d-sm-none.gl-mt-3.gl-mr-3
       = render "shared/mobile_clone_panel"
 
-    .project-clone-holder.d-none.d-sm-inline-block.gl-mb-3.gl-mr-3.float-left
+    #empty-page.project-clone-holder.d-none.d-sm-inline-block.gl-mb-3.gl-mr-3.float-left
       = render "projects/buttons/code", ref: @ref
     = render 'stat_anchor_list', anchors: @project.empty_repo_statistics_buttons, project_buttons: true
 
diff --git a/ee/app/views/projects/buttons/_code.html.haml b/ee/app/views/projects/buttons/_code.html.haml
new file mode 100644
index 0000000000000000000000000000000000000000..8e1352a33a12def22c1c955546366fac1b0c2389
--- /dev/null
+++ b/ee/app/views/projects/buttons/_code.html.haml
@@ -0,0 +1,13 @@
+- project = project || @project
+- ref = local_assigns.fetch(:ref) || ''
+- archive_prefix = ref ? "#{project.path}-#{ref.tr('/', '-')}" : ''
+
+- if can?(current_user, :download_code, @project)
+  .git-clone-holder.js-git-clone-holder
+    #js-code-dropdown{ data: {
+      ssh_url: ssh_enabled? ? ssh_clone_url_to_repo(@project) : '',
+      http_url: http_enabled? ? http_clone_url_to_repo(@project) : '',
+      kerberos_url: alternative_kerberos_url? ? project.kerberos_url_to_repo : '',
+      xcode_url: show_xcode_link?(@project) ? xcode_uri_to_repo(@project) : '',
+      directory_download_links: directory_download_links(project, ref, archive_prefix).to_json,
+    } }
diff --git a/ee/spec/features/projects/kerberos_clone_instructions_spec.rb b/ee/spec/features/projects/kerberos_clone_instructions_spec.rb
index 618e1a85262c634d18a4a1a644fa377a1e745c8f..b7c4b7acadb8951f2b538eda0b1090333ca7cf0c 100644
--- a/ee/spec/features/projects/kerberos_clone_instructions_spec.rb
+++ b/ee/spec/features/projects/kerberos_clone_instructions_spec.rb
@@ -16,11 +16,11 @@
   it 'shows Kerberos clone url' do
     visit_project
 
-    find('.clone-dropdown-btn').click
+    find_by_testid('code-dropdown').click
 
     expect(page).to have_content(project.kerberos_url_to_repo)
 
-    within('.git-clone-holder') do
+    within_testid('code-dropdown') do
       expect(page).to have_content('Clone with KRB5')
     end
   end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index b025513cce95b335e4a56ec41834ef8c2c0aa377..d732a25fa96a4d465c25208d7fdf1e2cf9e26965 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -10998,9 +10998,6 @@ msgstr ""
 msgid "Clone this issue"
 msgstr ""
 
-msgid "Clone with %{http_label}"
-msgstr ""
-
 msgid "Clone with %{protocol}"
 msgstr ""
 
@@ -18208,6 +18205,9 @@ msgstr ""
 msgid "Download PDF"
 msgstr ""
 
+msgid "Download Source Code"
+msgstr ""
+
 msgid "Download artifacts"
 msgstr ""
 
diff --git a/qa/qa/page/component/clone_panel.rb b/qa/qa/page/component/clone_panel.rb
deleted file mode 100644
index 4d239040faa310fcf60880af2662253c94a96377..0000000000000000000000000000000000000000
--- a/qa/qa/page/component/clone_panel.rb
+++ /dev/null
@@ -1,42 +0,0 @@
-# frozen_string_literal: true
-
-module QA
-  module Page
-    module Component
-      module ClonePanel
-        extend QA::Page::PageConcern
-
-        def self.included(base)
-          super
-
-          base.view 'app/views/projects/buttons/_code.html.haml' do
-            element 'clone-dropdown'
-            element 'clone-dropdown-content'
-            element 'ssh-clone-url-content'
-            element 'http-clone-url-content'
-          end
-        end
-
-        def repository_clone_http_location
-          repository_clone_location('http-clone-url-content')
-        end
-
-        def repository_clone_ssh_location
-          repository_clone_location('ssh-clone-url-content')
-        end
-
-        private
-
-        def repository_clone_location(kind)
-          wait_until(reload: false) do
-            click_element 'clone-dropdown'
-
-            within_element 'clone-dropdown-content' do
-              Git::Location.new(find_element(kind).value)
-            end
-          end
-        end
-      end
-    end
-  end
-end
diff --git a/qa/qa/page/project/show.rb b/qa/qa/page/project/show.rb
index 36c14054188d9bf1566f6244908f4d31e9e31b07..fac8762e5eb3b9354362a0d4fe4f0accf6fc91a4 100644
--- a/qa/qa/page/project/show.rb
+++ b/qa/qa/page/project/show.rb
@@ -5,7 +5,6 @@ module Page
     module Project
       class Show < Page::Base
         include Layout::Flash
-        include Page::Component::ClonePanel
         include Page::Component::Breadcrumbs
         include Page::File::Shared::CommitMessage
         include Page::Component::Dropdown
diff --git a/spec/features/admin/admin_disables_git_access_protocol_spec.rb b/spec/features/admin/admin_disables_git_access_protocol_spec.rb
index 039968025a92c97beab9d89edccaa4e6040ab563..6a694376d84c4d7c1c5437e75f916f095c4cd95a 100644
--- a/spec/features/admin/admin_disables_git_access_protocol_spec.rb
+++ b/spec/features/admin/admin_disables_git_access_protocol_spec.rb
@@ -25,9 +25,9 @@
 
       expect(page).to have_content("git clone #{project.ssh_url_to_repo}")
 
-      find('.clone-dropdown-btn').click
+      find('[data-testid="code-dropdown"] button').click
 
-      within('.git-clone-holder') do
+      within_testid('code-dropdown') do
         expect(page).to have_content('Clone with SSH')
         expect(page).not_to have_content('Clone with HTTP')
       end
@@ -55,11 +55,12 @@
 
     it 'shows only HTTP url' do
       visit_project
-      find('.clone-dropdown-btn').click
+
+      find('[data-testid="code-dropdown"] button').click
 
       expect(page).to have_content("git clone #{project.http_url_to_repo}")
 
-      within('.git-clone-holder') do
+      within_testid('code-dropdown') do
         expect(page).to have_content('Clone with HTTP')
         expect(page).not_to have_content('Clone with SSH')
       end
@@ -91,9 +92,9 @@
 
       expect(page).to have_content("git clone #{project.ssh_url_to_repo}")
 
-      find('.clone-dropdown-btn').click
+      find('[data-testid="code-dropdown"] button').click
 
-      within('.git-clone-holder') do
+      within_testid('code-dropdown') do
         expect(page).to have_content('Clone with SSH')
         expect(page).to have_content('Clone with HTTP')
       end
diff --git a/spec/features/projects/show/clone_button_spec.rb b/spec/features/projects/show/clone_button_spec.rb
index 83e75101427edeaa293f73974ec0f8dac9a8dc15..0dea5887c013a8dcd355b6d392f06afb904bb368 100644
--- a/spec/features/projects/show/clone_button_spec.rb
+++ b/spec/features/projects/show/clone_button_spec.rb
@@ -2,7 +2,7 @@
 
 require 'spec_helper'
 
-RSpec.describe 'Projects > Show > Clone button', feature_category: :groups_and_projects do
+RSpec.describe 'Projects > Show > Code button', feature_category: :groups_and_projects do
   let_it_be(:admin) { create(:admin) }
   let_it_be(:guest) { create(:user) }
   let_it_be(:project) { create(:project, :private, :in_group, :repository) }
@@ -19,10 +19,10 @@
         expect(page).to have_content project.name
       end
 
-      it 'sees clone button', :js do
-        find_by_testid('clone-dropdown').click
-        expect(page).to have_content _('Clone')
-        expect(page).to be_axe_clean.within('.clone-options-dropdown')
+      it 'sees code button', :js do
+        find_by_testid('code-dropdown').click
+        expect(page).to have_content _('Code')
+        expect(page).to be_axe_clean.within('[data-testid="code-dropdown"]') # rubocop: disable Capybara/TestidFinders -- within_testid does not work here
       end
     end
 
@@ -37,8 +37,8 @@
         expect(page).to have_content project.name
       end
 
-      it 'does not see clone button' do
-        expect(page).not_to have_content _('Clone')
+      it 'does not see code button' do
+        expect(page).not_to have_content _('Code')
       end
     end
   end
diff --git a/spec/features/projects/show/user_sees_git_instructions_spec.rb b/spec/features/projects/show/user_sees_git_instructions_spec.rb
index 40549beae9f80092ed56e1065bda8e0fc4936189..8d2f93ba3b7196af3e7a6943b0cbc376604c160a 100644
--- a/spec/features/projects/show/user_sees_git_instructions_spec.rb
+++ b/spec/features/projects/show/user_sees_git_instructions_spec.rb
@@ -34,13 +34,17 @@
   shared_examples_for 'shows details of empty project' do
     let(:user_has_ssh_key) { false }
 
-    it 'shows details' do
+    it 'shows details', :js do
       expect(page).not_to have_content('Git global setup')
 
       page.all(:css, '.git-empty .clone').each do |element|
         expect(element.text).to include(project.http_url_to_repo)
       end
 
+      find_by_testid('code-dropdown').click
+
+      wait_for_requests
+
       expect(page).to have_field('http_project_clone', with: project.http_url_to_repo) unless user_has_ssh_key
     end
   end
@@ -48,11 +52,15 @@
   shared_examples_for 'shows details of non empty project' do
     let(:user_has_ssh_key) { false }
 
-    it 'shows details' do
+    it 'shows details', :js do
       page.within('.breadcrumbs .js-breadcrumb-item-text') do
         expect(page).to have_content(project.title)
       end
 
+      find_by_testid('code-dropdown').click
+
+      wait_for_requests
+
       expect(page).to have_field('http_project_clone', with: project.http_url_to_repo) unless user_has_ssh_key
     end
   end
diff --git a/spec/frontend/vue_shared/components/code_dropdown/code_dropdown_spec.js b/spec/frontend/vue_shared/components/code_dropdown/code_dropdown_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..294d0f091420d4c63e1e8ebe28145de3be88102e
--- /dev/null
+++ b/spec/frontend/vue_shared/components/code_dropdown/code_dropdown_spec.js
@@ -0,0 +1,143 @@
+import { GlFormInputGroup, GlDisclosureDropdownGroup, GlDisclosureDropdownItem } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import CodeDropdown from '~/vue_shared/components/code_dropdown/code_dropdown.vue';
+import CloneDropdownItem from '~/vue_shared/components/clone_dropdown/clone_dropdown_item.vue';
+
+describe('Clone Dropdown Button', () => {
+  let wrapper;
+  const sshUrl = 'ssh://foo.bar';
+  const httpUrl = 'http://foo.bar';
+  const httpsUrl = 'https://foo.bar';
+  const xcodeUrl = 'xcode://foo.bar';
+  const currentPath = null;
+  const directoryDownloadLinks = [
+    { text: 'zip', path: httpUrl },
+    { text: 'tar.gz', path: httpUrl },
+    { text: 'tar.bz2', path: httpUrl },
+    { text: 'tar', path: httpUrl },
+  ];
+  const defaultPropsData = {
+    sshUrl,
+    httpUrl,
+    xcodeUrl,
+    currentPath,
+    directoryDownloadLinks,
+  };
+  const encodedSshUrl = encodeURIComponent(sshUrl);
+  const encodedHttpUrl = encodeURIComponent(httpUrl);
+
+  const findCloneDropdownItems = () => wrapper.findAllComponents(CloneDropdownItem);
+  const findCloneDropdownItemAtIndex = (index) => findCloneDropdownItems().at(index);
+  const findDropdownItems = () => wrapper.findAllComponents(GlDisclosureDropdownItem);
+  const findDropdownItemAtIndex = (index) => findDropdownItems().at(index);
+
+  const createComponent = (propsData = defaultPropsData) => {
+    wrapper = shallowMount(CodeDropdown, {
+      propsData,
+      stubs: {
+        GlFormInputGroup,
+        GlDisclosureDropdownGroup,
+      },
+    });
+  };
+
+  describe('copyGroup', () => {
+    describe('rendering', () => {
+      it.each`
+        name      | index | link
+        ${'SSH'}  | ${0}  | ${sshUrl}
+        ${'HTTP'} | ${1}  | ${httpUrl}
+      `('renders correct link and a copy-button for $name', ({ index, link }) => {
+        createComponent();
+
+        const item = findCloneDropdownItemAtIndex(index);
+        expect(item.props('link')).toBe(link);
+      });
+
+      it.each`
+        name         | value
+        ${'sshUrl'}  | ${sshUrl}
+        ${'httpUrl'} | ${httpUrl}
+      `('does not fail if only $name is set', ({ name, value }) => {
+        createComponent({ [name]: value });
+
+        expect(findCloneDropdownItemAtIndex(0).props('link')).toBe(value);
+      });
+    });
+
+    describe('functionality', () => {
+      it.each`
+        name         | value
+        ${'sshUrl'}  | ${null}
+        ${'httpUrl'} | ${null}
+      `('allows null values for the props', ({ name, value }) => {
+        createComponent({ ...defaultPropsData, [name]: value });
+
+        expect(findCloneDropdownItems().length).toBe(1);
+      });
+
+      it('correctly calculates httpLabel for HTTPS protocol', () => {
+        createComponent({ httpUrl: httpsUrl });
+
+        expect(findCloneDropdownItemAtIndex(0).attributes('label')).toContain('HTTPS');
+      });
+    });
+  });
+
+  describe('ideGroup', () => {
+    it.each`
+      name                            | index | href
+      ${'Visual Studio Code (SSH)'}   | ${0}  | ${encodedSshUrl}
+      ${'Visual Studio Code (HTTPS)'} | ${1}  | ${encodedHttpUrl}
+      ${'IntelliJ IDEA (SSH)'}        | ${2}  | ${encodedSshUrl}
+      ${'IntelliJ IDEA (HTTPS)'}      | ${3}  | ${encodedHttpUrl}
+      ${'Xcode'}                      | ${4}  | ${xcodeUrl}
+    `('renders correct values for $name', ({ name, index, href }) => {
+      createComponent();
+
+      const item = findDropdownItemAtIndex(index);
+      expect(item.props('item').text).toBe(name);
+      expect(item.props('item').href).toContain(href);
+    });
+  });
+
+  describe('sourceCodeGroup', () => {
+    it.each`
+      name         | index | href
+      ${'zip'}     | ${5}  | ${httpUrl}
+      ${'tar.gz'}  | ${6}  | ${httpUrl}
+      ${'tar.bz2'} | ${7}  | ${httpUrl}
+      ${'tar'}     | ${8}  | ${httpUrl}
+    `('renders correct values for $name', ({ name, index, href }) => {
+      createComponent();
+
+      const item = findDropdownItemAtIndex(index);
+      expect(item.props('item').text).toBe(name);
+      expect(item.props('item').href).toBe(href);
+    });
+  });
+
+  describe('directoryDownloadLinksGroup', () => {
+    it('renders directory download links if currentPath is set', () => {
+      createComponent({ ...defaultPropsData, currentPath: '/subdir' });
+
+      expect(findDropdownItems().length).toEqual(13);
+    });
+
+    it.each`
+      name         | index | href
+      ${'zip'}     | ${9}  | ${httpUrl}
+      ${'tar.gz'}  | ${10} | ${httpUrl}
+      ${'tar.bz2'} | ${11} | ${httpUrl}
+      ${'tar'}     | ${12} | ${httpUrl}
+    `('renders correct values for $name directory link', ({ name, index, href }) => {
+      const subPath = '/subdir';
+
+      createComponent({ ...defaultPropsData, currentPath: subPath });
+
+      const item = findDropdownItemAtIndex(index);
+      expect(item.props('item').text).toBe(name);
+      expect(item.props('item').href).toBe(`${href}?path=${subPath}`);
+    });
+  });
+});