From 20453451d869316c03b9e16b1af9a91cd10f25c6 Mon Sep 17 00:00:00 2001 From: cookesan <6601329+cookesan@users.noreply.github.com> Date: Sun, 28 Jun 2026 07:15:11 -0400 Subject: [PATCH 1/3] fix: stop hidden copy button blocking code text --- .../markdown-extensions.test.ts | 39 +++++++++++++++++++ .../styles/components/vp-doc.css | 2 + 2 files changed, 41 insertions(+) diff --git a/__tests__/e2e/markdown-extensions/markdown-extensions.test.ts b/__tests__/e2e/markdown-extensions/markdown-extensions.test.ts index 839f953c..4a510a23 100644 --- a/__tests__/e2e/markdown-extensions/markdown-extensions.test.ts +++ b/__tests__/e2e/markdown-extensions/markdown-extensions.test.ts @@ -167,6 +167,45 @@ describe('Custom Containers', () => { }) describe('Line Highlighting in Code Blocks', () => { + test('hidden copy button does not block code text', async () => { + await page.setViewportSize({ width: 390, height: 844 }) + await goto('/markdown-extensions/') + + const block = page.locator('#single-line + div') + const button = block.locator('> button.copy') + await block.scrollIntoViewIfNeeded() + const buttonBox = await button.boundingBox() + expect(buttonBox).toBeTruthy() + + const isCopyButtonAtPoint = await page.evaluate( + ({ x, y }) => + document.elementFromPoint(x, y)?.matches('button.copy') ?? false, + { + x: buttonBox!.x + buttonBox!.width / 2, + y: buttonBox!.y + buttonBox!.height / 2 + } + ) + + expect(isCopyButtonAtPoint).toBe(false) + + await page.setViewportSize({ width: 1280, height: 720 }) + await goto('/markdown-extensions/') + const desktopBlock = page.locator('#single-line + div') + const desktopButton = desktopBlock.locator('> button.copy') + + await desktopBlock.hover() + await page.waitForFunction( + (el) => getComputedStyle(el).opacity === '1', + await desktopButton.elementHandle() + ) + expect( + await desktopButton.evaluate((el) => getComputedStyle(el).opacity) + ).toBe('1') + expect( + await desktopButton.evaluate((el) => getComputedStyle(el).pointerEvents) + ).toBe('auto') + }) + test('single line', async () => { const classList = await getClassList( page.locator('#single-line + div code > span').nth(3) diff --git a/src/client/theme-default/styles/components/vp-doc.css b/src/client/theme-default/styles/components/vp-doc.css index 904427fc..56893dbd 100644 --- a/src/client/theme-default/styles/components/vp-doc.css +++ b/src/client/theme-default/styles/components/vp-doc.css @@ -455,6 +455,7 @@ height: 40px; background-color: var(--vp-code-copy-code-bg); opacity: 0; + pointer-events: none; cursor: pointer; background-image: var(--vp-icon-copy); background-position: 50%; @@ -469,6 +470,7 @@ .vp-doc [class*='language-']:hover > button.copy, .vp-doc [class*='language-'] > button.copy:focus { opacity: 1; + pointer-events: auto; } .vp-doc [class*='language-'] > button.copy:hover, From 6fa626cbd3f8f4105e8873c50341e66b2b249b5c Mon Sep 17 00:00:00 2001 From: cookesan <6601329+cookesan@users.noreply.github.com> Date: Sun, 28 Jun 2026 07:41:17 -0400 Subject: [PATCH 2/3] fix: keep mobile copy button above code text --- .../markdown-extensions.test.ts | 27 ++++++++++++------- .../styles/components/vp-doc.css | 10 +++++++ 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/__tests__/e2e/markdown-extensions/markdown-extensions.test.ts b/__tests__/e2e/markdown-extensions/markdown-extensions.test.ts index 4a510a23..46a1b693 100644 --- a/__tests__/e2e/markdown-extensions/markdown-extensions.test.ts +++ b/__tests__/e2e/markdown-extensions/markdown-extensions.test.ts @@ -167,26 +167,35 @@ describe('Custom Containers', () => { }) describe('Line Highlighting in Code Blocks', () => { - test('hidden copy button does not block code text', async () => { + test('copy button does not overlap mobile code text', async () => { await page.setViewportSize({ width: 390, height: 844 }) await goto('/markdown-extensions/') const block = page.locator('#single-line + div') const button = block.locator('> button.copy') + const firstLine = block.locator('code > span').first() await block.scrollIntoViewIfNeeded() const buttonBox = await button.boundingBox() + const firstLineBox = await firstLine.boundingBox() expect(buttonBox).toBeTruthy() + expect(firstLineBox).toBeTruthy() + expect(buttonBox!.y + buttonBox!.height).toBeLessThanOrEqual( + firstLineBox!.y + ) - const isCopyButtonAtPoint = await page.evaluate( - ({ x, y }) => - document.elementFromPoint(x, y)?.matches('button.copy') ?? false, - { - x: buttonBox!.x + buttonBox!.width / 2, - y: buttonBox!.y + buttonBox!.height / 2 - } + await block.hover() + await page.waitForFunction( + (el) => getComputedStyle(el).opacity === '1', + await button.elementHandle() ) - expect(isCopyButtonAtPoint).toBe(false) + const hoveredButtonBox = await button.boundingBox() + const hoveredFirstLineBox = await firstLine.boundingBox() + expect(hoveredButtonBox).toBeTruthy() + expect(hoveredFirstLineBox).toBeTruthy() + expect(hoveredButtonBox!.y + hoveredButtonBox!.height).toBeLessThanOrEqual( + hoveredFirstLineBox!.y + ) await page.setViewportSize({ width: 1280, height: 720 }) await goto('/markdown-extensions/') diff --git a/src/client/theme-default/styles/components/vp-doc.css b/src/client/theme-default/styles/components/vp-doc.css index 56893dbd..11a39b9b 100644 --- a/src/client/theme-default/styles/components/vp-doc.css +++ b/src/client/theme-default/styles/components/vp-doc.css @@ -473,6 +473,16 @@ pointer-events: auto; } +@media (max-width: 639px) { + .vp-doc [class*='language-'] pre { + padding-top: 64px; + } + + .vp-doc div[class*='language-'].line-numbers-mode .line-numbers-wrapper { + padding-top: 64px; + } +} + .vp-doc [class*='language-'] > button.copy:hover, .vp-doc [class*='language-'] > button.copy.copied { border-color: var(--vp-code-copy-code-hover-border-color); From ca9c41024c956a02a97e62222603399119b9badf Mon Sep 17 00:00:00 2001 From: cookesan <6601329+cookesan@users.noreply.github.com> Date: Sun, 28 Jun 2026 08:37:09 -0400 Subject: [PATCH 3/3] fix: move mobile copy button outside code block --- __tests__/e2e/markdown-extensions/index.md | 2 +- .../markdown-extensions.test.ts | 77 +++++++++++++------ .../styles/components/vp-doc.css | 43 ++++++++++- 3 files changed, 94 insertions(+), 28 deletions(-) diff --git a/__tests__/e2e/markdown-extensions/index.md b/__tests__/e2e/markdown-extensions/index.md index 3446b4ef..c5202885 100644 --- a/__tests__/e2e/markdown-extensions/index.md +++ b/__tests__/e2e/markdown-extensions/index.md @@ -169,7 +169,7 @@ export default config <<< @/markdown-extensions/foo.md -<<< @/markdown-extensions/foo.md#snippet{1 ts:line-numbers} [snippet with region] +<<< @/markdown-extensions/foo.md#snippet{1 ts:line-numbers} [snippet with region and a long mobile tab label] ::: diff --git a/__tests__/e2e/markdown-extensions/markdown-extensions.test.ts b/__tests__/e2e/markdown-extensions/markdown-extensions.test.ts index 46a1b693..48b1a232 100644 --- a/__tests__/e2e/markdown-extensions/markdown-extensions.test.ts +++ b/__tests__/e2e/markdown-extensions/markdown-extensions.test.ts @@ -7,6 +7,30 @@ const getClassList = async (locator: Locator) => { const trim = (str?: string | null) => str?.replace(/\u200B/g, '').trim() +const expectMobileCopyButtonOutsideCode = async ( + block: Locator, + limit?: Locator +) => { + const button = block.locator('> button.copy') + const firstLine = block.locator('code > span').first() + await block.scrollIntoViewIfNeeded() + + const [buttonBox, firstLineBox, limitBox] = await Promise.all([ + button.boundingBox(), + firstLine.boundingBox(), + (limit ?? block).boundingBox() + ]) + expect(buttonBox).toBeTruthy() + expect(firstLineBox).toBeTruthy() + expect(limitBox).toBeTruthy() + expect(buttonBox!.y + buttonBox!.height).toBeLessThanOrEqual(limitBox!.y) + expect(buttonBox!.y + buttonBox!.height).toBeLessThanOrEqual(firstLineBox!.y) + expect(await button.evaluate((el) => getComputedStyle(el).opacity)).toBe('1') + expect( + await button.evaluate((el) => getComputedStyle(el).pointerEvents) + ).toBe('auto') +} + beforeEach(async () => { await goto('/markdown-extensions/') }) @@ -164,6 +188,17 @@ describe('Custom Containers', () => { 'Click me to view the code' ) }) + + test('copy button stays outside mobile custom container code', async () => { + await page.setViewportSize({ width: 390, height: 844 }) + await goto('/markdown-extensions/') + + const block = page.locator('#custom-title ~ .custom-block').nth(1) + await block.locator('summary').click() + await expectMobileCopyButtonOutsideCode( + block.locator('div[class*="language-"]') + ) + }) }) describe('Line Highlighting in Code Blocks', () => { @@ -172,30 +207,10 @@ describe('Line Highlighting in Code Blocks', () => { await goto('/markdown-extensions/') const block = page.locator('#single-line + div') - const button = block.locator('> button.copy') - const firstLine = block.locator('code > span').first() - await block.scrollIntoViewIfNeeded() - const buttonBox = await button.boundingBox() - const firstLineBox = await firstLine.boundingBox() - expect(buttonBox).toBeTruthy() - expect(firstLineBox).toBeTruthy() - expect(buttonBox!.y + buttonBox!.height).toBeLessThanOrEqual( - firstLineBox!.y - ) + await expectMobileCopyButtonOutsideCode(block) await block.hover() - await page.waitForFunction( - (el) => getComputedStyle(el).opacity === '1', - await button.elementHandle() - ) - - const hoveredButtonBox = await button.boundingBox() - const hoveredFirstLineBox = await firstLine.boundingBox() - expect(hoveredButtonBox).toBeTruthy() - expect(hoveredFirstLineBox).toBeTruthy() - expect(hoveredButtonBox!.y + hoveredButtonBox!.height).toBeLessThanOrEqual( - hoveredFirstLineBox!.y - ) + await expectMobileCopyButtonOutsideCode(block) await page.setViewportSize({ width: 1280, height: 720 }) await goto('/markdown-extensions/') @@ -307,7 +322,10 @@ describe('Code Groups', () => { // tabs const labels = div.locator('.tabs > label') - const labelNames = ['foo.md', 'snippet with region'] + const labelNames = [ + 'foo.md', + 'snippet with region and a long mobile tab label' + ] const count = await labels.count() expect(count).toBe(2) for (let i = 0; i < count; i++) { @@ -325,6 +343,19 @@ describe('Code Groups', () => { await getClassList(blocks.nth(1).locator('code > span').nth(0)) ).toContain('highlighted') }) + + test('copy button stays outside mobile code group tabs', async () => { + await page.setViewportSize({ width: 390, height: 844 }) + await goto('/markdown-extensions/') + + const div = page.locator('#with-other-features-1 + div') + await div.scrollIntoViewIfNeeded() + await div.locator('.tabs > label').nth(1).click() + await expectMobileCopyButtonOutsideCode( + div.locator('.blocks > div.active'), + div.locator('> .tabs') + ) + }) }) describe('Markdown File Inclusion', () => { diff --git a/src/client/theme-default/styles/components/vp-doc.css b/src/client/theme-default/styles/components/vp-doc.css index 11a39b9b..3ad79b61 100644 --- a/src/client/theme-default/styles/components/vp-doc.css +++ b/src/client/theme-default/styles/components/vp-doc.css @@ -474,12 +474,47 @@ } @media (max-width: 639px) { - .vp-doc [class*='language-'] pre { - padding-top: 64px; + .vp-doc div[class*='language-'] { + margin-top: 64px; + overflow: visible; } - .vp-doc div[class*='language-'].line-numbers-mode .line-numbers-wrapper { - padding-top: 64px; + .vp-doc div[class*='language-'] + div[class*='language-'], + .vp-doc div[class$='-api'] + div[class*='language-'], + .vp-doc + div[class*='language-'] + + div[class$='-api'] + > div[class*='language-'] { + margin-top: 64px; + } + + .vp-doc .custom-block div[class*='language-'] { + margin-top: 64px !important; + } + + .vp-doc [class*='language-'] > button.copy { + top: -48px; + /*rtl:ignore*/ + right: 0; + opacity: 1; + pointer-events: auto; + } + + .vp-doc .vp-code-group { + margin-top: 64px; + } + + .vp-doc .custom-block .vp-code-group, + .vp-doc .custom-block [class*='vp-code-block'] { + margin-top: 64px; + } + + .vp-doc .vp-code-group div[class*='language-'] { + margin-top: 0 !important; + } + + .vp-doc .vp-code-group [class*='language-'] > button.copy { + top: -112px; } }