feat(muya): make code copy keyboard accessible with clipboard feedbac… · marktext/marktext@deb03a7 · GitHub
Skip to content

Commit deb03a7

Browse files
authored
feat(muya): make code copy keyboard accessible with clipboard feedback (#5800)
1 parent 4d5753a commit deb03a7

26 files changed

Lines changed: 196 additions & 25 deletions

File tree

‎packages/desktop/src/main/ipc/shell.ts‎

Lines changed: 10 additions & 4 deletions

‎packages/desktop/src/preload/index.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ const shellAPI = {
7676
}
7777

7878
const clipboardAPI = {
79-
writeText: (text: string) => send('mt::clipboard::write-text', text),
79+
writeText: (text: string) => invoke('mt::clipboard::write-text', text),
8080
readText: () => invoke('mt::clipboard::read-text'),
8181
guessFilePath: () => invoke('mt::clipboard::guess-file-path'),
8282
writeImage: (png: Uint8Array) => invoke('mt::clipboard::write-image', png)

‎packages/desktop/src/renderer/src/components/editorWithTabs/editor.vue‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1818,6 +1818,7 @@ onMounted(() => {
18181818
clipboardFilePath: guessClipboardFilePath,
18191819
// Read the OS clipboard's plain text for "Paste as Plain Text" (execCommand('paste') no longer fires).
18201820
clipboardText: () => window.electron.clipboard.readText(),
1821+
clipboardWriteText: window.electron.clipboard.writeText,
18211822
fetchLinkHtml: (url) => window.electron.fetchLinkHtml(url),
18221823
// Image-persist callbacks read by the engine's clipboard + drag-drop handlers
18231824
// from `muya.options.*` (distinct from the ImageEditTool plugin option above).

‎packages/desktop/src/shared/types/ipc.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ export type PlantumlFetchResult =
5050
// =================================================================
5151

5252
export interface IpcInvokeChannels {
53+
'mt::clipboard::write-text': { args: [text: string]; ret: boolean }
5354
'mt::ask-for-image-path': { args: []; ret: string[] }
5455
'mt::boot-info-async': { args: []; ret: BootInfo }
5556
'mt::clipboard::guess-file-path': { args: []; ret: string | null }
@@ -130,7 +131,6 @@ export interface IpcSendChannels {
130131
'mt::ask-for-user-data': []
131132
'mt::ask-for-user-preference': []
132133
'mt::check-for-update': []
133-
'mt::clipboard::write-text': [text: string]
134134
'mt::close-window': []
135135
'mt::close-window-confirm': [unsavedFiles: UnsavedFile[]]
136136
'mt::cmd-close-window': []

‎packages/desktop/src/types/global.d.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ declare global {
5151
}
5252

5353
interface ElectronClipboardAPI {
54-
writeText(text: string): void
54+
writeText(text: string): Promise<boolean>
5555
readText(): Promise<string>
5656
guessFilePath(): Promise<string | null>
5757
writeImage(png: Uint8Array): Promise<boolean>
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { expect, test } from '@playwright/test'
2+
import type { ElectronApplication, Page } from 'playwright'
3+
import { launchWithMarkdown } from './helpers'
4+
5+
let app: ElectronApplication
6+
let page: Page
7+
8+
test.beforeAll(async() => {
9+
const launched = await launchWithMarkdown('```js\nconst value = 1\n```\n', {
10+
preferences: { language: 'en' }
11+
})
12+
app = launched.app
13+
page = launched.page
14+
})
15+
16+
test.afterEach(async() => {
17+
await app.evaluate(({ clipboard }) => {
18+
const state = globalThis as typeof globalThis & {
19+
__originalWriteText?: typeof clipboard.writeText
20+
}
21+
if (state.__originalWriteText) {
22+
clipboard.writeText = state.__originalWriteText
23+
delete state.__originalWriteText
24+
}
25+
})
26+
})
27+
28+
test.afterAll(async() => {
29+
if (app) await app.close()
30+
})
31+
32+
test('the shared clipboard API returns success without a duplicate send listener', async() => {
33+
expect(await page.evaluate(() => window.electron.clipboard.writeText('shared clipboard API'))).toBe(true)
34+
expect(await app.evaluate(({ clipboard }) => clipboard.readText())).toBe('shared clipboard API')
35+
expect(await app.evaluate(({ ipcMain }) => ipcMain.listenerCount('mt::clipboard::write-text'))).toBe(0)
36+
})
37+
38+
test('code copying uses the shared API and reports success', async() => {
39+
await page.locator('.mu-code-block').hover()
40+
const button = page.locator('button.mu-code-copy')
41+
await button.click()
42+
await expect(button).toHaveAttribute('data-copy-feedback', 'Copied')
43+
expect(await app.evaluate(({ clipboard }) => clipboard.readText())).toBe('const value = 1')
44+
})
45+
46+
test('native clipboard failures return false and show failure feedback', async() => {
47+
await app.evaluate(({ clipboard }) => {
48+
const state = globalThis as typeof globalThis & {
49+
__originalWriteText?: typeof clipboard.writeText
50+
}
51+
state.__originalWriteText = clipboard.writeText
52+
clipboard.writeText = () => { throw new Error('clipboard unavailable') }
53+
})
54+
expect(await page.evaluate(() => window.electron.clipboard.writeText('not copied'))).toBe(false)
55+
await page.locator('.mu-code-block').hover()
56+
const button = page.locator('button.mu-code-copy')
57+
await button.click()
58+
await expect(button).toHaveAttribute('data-copy-feedback', 'Copy failed. Please try again.')
59+
})

‎packages/desktop/test/e2e/issue-5477-editor-text-cursor.spec.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ test('the scroll area outside the text column keeps the arrow', async() => {
8282

8383
test('clickable content keeps the hand cursor', async() => {
8484
for (const selector of [
85-
'.mu-container a',
85+
'.mu-container button.mu-code-copy',
8686
'.mu-container input[type=checkbox]'
8787
]) {
8888
expect(await cursorOf(selector), selector).toBe('pointer')

‎packages/muya/src/__tests__/localeCompleteness.spec.ts‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,16 @@ describe('locale completeness', () => {
9696
}
9797
});
9898

99+
describe('clipboard translations', () => {
100+
for (const [tag, locale] of nonEnLocales) {
101+
it(`${tag} translates clipboard messages`, () => {
102+
for (const key of ['Copied', 'Copy failed'] as const) {
103+
expect(locale.resource[key], `${tag}: ${key}`).not.toBe(en.resource[key]);
104+
}
105+
});
106+
}
107+
});
108+
99109
describe('every resource value is a non-empty string', () => {
100110
for (const [tag, locale] of [['en', en] as [string, typeof en], ...nonEnLocales]) {
101111
it(`${tag} has only non-empty string values`, () => {

‎packages/muya/src/assets/styles/index.css‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,3 +273,31 @@ body {
273273

274274
background: var(--highlight-color);
275275
}
276+
277+
.mu-code-copy {
278+
background: transparent;
279+
border: 0;
280+
}
281+
282+
.mu-code-block:hover > .mu-code-copy,
283+
.mu-code-block > .mu-code-copy:focus-visible {
284+
opacity: 1;
285+
}
286+
287+
.mu-code-copy[data-copy-feedback]::after {
288+
position: absolute;
289+
right: 0;
290+
bottom: 100%;
291+
292+
padding: 4px 8px;
293+
294+
color: var(--editor-color);
295+
font: 12px sans-serif;
296+
white-space: nowrap;
297+
298+
background: var(--float-bg-color);
299+
border-radius: 4px;
300+
box-shadow: var(--float-shadow);
301+
302+
content: attr(data-copy-feedback);
303+
}

‎packages/muya/src/block/commonMark/codeBlock/__tests__/copyButton.spec.ts‎

Lines changed: 20 additions & 11 deletions

0 commit comments

Comments
 (0)