diff --git a/e2e/resume-linkedin.spec.ts b/e2e/resume-linkedin.spec.ts index ff74ba696..8b8b0cf14 100644 --- a/e2e/resume-linkedin.spec.ts +++ b/e2e/resume-linkedin.spec.ts @@ -104,4 +104,31 @@ test.describe('Resume LinkedIn Format Page', () => { // Should have at least one newline per bullet (bullets appear after newlines) expect(newlineCount).toBeGreaterThanOrEqual(bulletCount - 1); }); + + test('should copy field text to clipboard when copy button is clicked', async ({ page, context }) => { + // Grant clipboard permissions for Chromium + await context.grantPermissions(['clipboard-read', 'clipboard-write']); + + // Click the first non-description copy button (e.g., Title) + const firstCopyBtn = page.locator('.copy-btn:not(.copy-description)').first(); + const expectedText = await firstCopyBtn.getAttribute('data-copy'); + await firstCopyBtn.click(); + + // Verify the button shows success feedback + await expect(firstCopyBtn).toHaveText('✓'); + + // Read from clipboard and verify + const clipboardText = await page.evaluate(() => navigator.clipboard.readText()); + expect(clipboardText).toBe(expectedText); + }); + + test('should restore focus to copy button after clicking', async ({ page, context }) => { + await context.grantPermissions(['clipboard-read', 'clipboard-write']); + + const firstCopyBtn = page.locator('.copy-btn').first(); + await firstCopyBtn.click(); + + // The copy button should retain focus after the copy operation + await expect(firstCopyBtn).toBeFocused(); + }); }); diff --git a/src/pages/resume/linkedin.astro b/src/pages/resume/linkedin.astro index de7cbabb0..57c7068c0 100644 --- a/src/pages/resume/linkedin.astro +++ b/src/pages/resume/linkedin.astro @@ -224,41 +224,5 @@ const educationData: EducationData[] = degrees?.map((degree: { school: string; d diff --git a/src/scripts/linkedin-copy.ts b/src/scripts/linkedin-copy.ts new file mode 100644 index 000000000..06e2b9836 --- /dev/null +++ b/src/scripts/linkedin-copy.ts @@ -0,0 +1,73 @@ +/** + * Copy text to clipboard with fallback for Safari/iPad OS. + * Safari silently resolves navigator.clipboard.writeText() without actually + * writing to the clipboard (the promise succeeds but content is blank), so we + * cannot use the modern Clipboard API with a catch fallback. Instead, we use + * textarea + execCommand('copy') which works reliably across all browsers. + */ +function copyToClipboard(text: string, triggerElement?: Element): boolean { + const textarea = document.createElement('textarea'); + textarea.value = text; + textarea.setAttribute('readonly', ''); + textarea.style.position = 'fixed'; + textarea.style.left = '-9999px'; + textarea.style.top = '-9999px'; + textarea.style.opacity = '0'; + document.body.appendChild(textarea); + + // iOS/iPad Safari requires both selection methods + textarea.select(); + textarea.setSelectionRange(0, textarea.value.length); + + let success = false; + try { + success = document.execCommand('copy'); + } catch { + success = false; + } + document.body.removeChild(textarea); + + // Restore focus to the trigger element after textarea removal + if (triggerElement instanceof HTMLElement) { + triggerElement.focus(); + } + + return success; +} + +function initCopyButtons() { + document.querySelectorAll('.copy-btn').forEach(btn => { + btn.addEventListener('click', () => { + let textToCopy = ''; + + if (btn.classList.contains('copy-description')) { + const descriptionElement = btn.parentElement?.querySelector('[data-description]') as HTMLElement | null; + textToCopy = descriptionElement?.innerText?.trim() || ''; + } else { + textToCopy = btn.getAttribute('data-copy') || ''; + } + + const success = copyToClipboard(textToCopy, btn); + + if (success) { + const original = btn.textContent; + btn.textContent = '✓'; + btn.classList.add('!bg-green-600'); + + setTimeout(() => { + btn.textContent = original; + btn.classList.remove('!bg-green-600'); + }, 1500); + } else { + const original = btn.textContent; + btn.textContent = '✗'; + setTimeout(() => { + btn.textContent = original; + }, 1500); + } + }); + }); +} + +// Re-initialize after Astro view transitions +document.addEventListener('astro:page-load', initCopyButtons);