diff --git a/app/components/CopyToClipboardButton.vue b/app/components/CopyToClipboardButton.vue
index f709aad730..9d342c01c0 100644
--- a/app/components/CopyToClipboardButton.vue
+++ b/app/components/CopyToClipboardButton.vue
@@ -16,8 +16,12 @@ const props = defineProps<{
const buttonCopyText = computed(() => props.copyText || $t('common.copy'))
const buttonCopiedText = computed(() => props.copiedText || $t('common.copied'))
-const buttonAriaLabelCopy = computed(() => props.ariaLabelCopy || $t('common.copy'))
-const buttonAriaLabelCopied = computed(() => props.ariaLabelCopied || $t('common.copied'))
+const buttonAriaLabelCopy = computed(
+ () => props.ariaLabelCopy || props.copyText || $t('common.copy'),
+)
+const buttonAriaLabelCopied = computed(
+ () => props.ariaLabelCopied || props.copiedText || $t('common.copied'),
+)
const emit = defineEmits<{
click: []
@@ -85,6 +89,7 @@ function handleClick() {
}
@media (hover: none) {
+ /* On touch devices, hide the button since hover is not available */
.copyButton {
display: none;
}
diff --git a/app/components/Package/Header.vue b/app/components/Package/Header.vue
index 7cf6b4c8bc..b04ad50205 100644
--- a/app/components/Package/Header.vue
+++ b/app/components/Package/Header.vue
@@ -253,7 +253,7 @@ const fundingUrl = computed(() => {
class="py-1.5 px-2.5 sm:me-2"
:tabindex="showScrollToTop ? 0 : -1"
/>
-
+
diff --git a/test/nuxt/components/CopyToClipboardButton.spec.ts b/test/nuxt/components/CopyToClipboardButton.spec.ts
new file mode 100644
index 0000000000..138ee12343
--- /dev/null
+++ b/test/nuxt/components/CopyToClipboardButton.spec.ts
@@ -0,0 +1,60 @@
+import { describe, expect, it } from 'vitest'
+import { mountSuspended } from '@nuxt/test-utils/runtime'
+import CopyToClipboardButton from '~/components/CopyToClipboardButton.vue'
+
+describe('CopyToClipboardButton', () => {
+ it('aria-label matches visible copy text when copyText is provided', async () => {
+ const wrapper = await mountSuspended(CopyToClipboardButton, {
+ props: {
+ copied: false,
+ copyText: 'Copy package name',
+ },
+ })
+
+ const button = wrapper.find('button')
+ expect(button.attributes('aria-label')).toBe('Copy package name')
+ expect(button.text()).toContain('Copy package name')
+ })
+
+ it('aria-label uses ariaLabelCopy when explicitly provided', async () => {
+ const wrapper = await mountSuspended(CopyToClipboardButton, {
+ props: {
+ copied: false,
+ copyText: 'Copy package name',
+ ariaLabelCopy: 'Copy the package name to clipboard',
+ },
+ })
+
+ const button = wrapper.find('button')
+ expect(button.attributes('aria-label')).toBe('Copy the package name to clipboard')
+ })
+
+ it('aria-label reflects copiedText when copied is true', async () => {
+ const wrapper = await mountSuspended(CopyToClipboardButton, {
+ props: {
+ copied: true,
+ copyText: 'Copy package name',
+ copiedText: 'Copied!',
+ },
+ })
+
+ const button = wrapper.find('button')
+ expect(button.attributes('aria-label')).toBe('Copied!')
+ expect(button.text()).toContain('Copied!')
+ })
+
+ it('aria-label matches visible text - no label/content mismatch', async () => {
+ const wrapper = await mountSuspended(CopyToClipboardButton, {
+ props: {
+ copied: false,
+ copyText: 'Copy install command',
+ },
+ })
+
+ const button = wrapper.find('button')
+ const ariaLabel = button.attributes('aria-label') ?? ''
+ const visibleText = button.text()
+ // The aria-label should equal the visible text (not some other string)
+ expect(visibleText).toContain(ariaLabel)
+ })
+})