diff --git a/__tests__/e2e/markdown-extensions/index.md b/__tests__/e2e/markdown-extensions/index.md index c95d60e0..149b1d4b 100644 --- a/__tests__/e2e/markdown-extensions/index.md +++ b/__tests__/e2e/markdown-extensions/index.md @@ -181,7 +181,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 000ede05..26a215a4 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/') }) @@ -166,9 +190,48 @@ 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', () => { + 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') + await expectMobileCopyButtonOutsideCode(block) + + await block.hover() + await expectMobileCopyButtonOutsideCode(block) + + 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) @@ -261,7 +324,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++) { @@ -279,6 +345,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 a1d85d90..7321392f 100644 --- a/src/client/theme-default/styles/components/vp-doc.css +++ b/src/client/theme-default/styles/components/vp-doc.css @@ -510,6 +510,7 @@ height: 2.5rem; background-color: var(--vp-code-copy-code-bg); opacity: 0; + pointer-events: none; cursor: pointer; background-image: var(--vp-icon-copy); background-position: 50%; @@ -524,6 +525,52 @@ .vp-doc [class*='language-']:hover > button.copy, .vp-doc [class*='language-'] > button.copy:focus { opacity: 1; + pointer-events: auto; +} + +@media (max-width: 639px) { + .vp-doc div[class*='language-'] { + margin-top: 64px; + overflow: visible; + } + + .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; + } } .vp-doc [class*='language-'] > button.copy:hover,