Skip to content

Commit 37f95a4

Browse files
authored
Add cmd/ctrl+shift+P keyboard shortcut for toggling views
1 parent c527a23 commit 37f95a4

File tree

3 files changed

+62
-3
lines changed

3 files changed

+62
-3
lines changed

src/drafts/MarkdownEditor/MarkdownEditor.test.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,22 @@ describe('MarkdownEditor', () => {
231231
expect(getInput()).toHaveAttribute('name', 'Name')
232232
})
233233

234+
describe('toggles between view modes on ctrl/cmd+shift+P', () => {
235+
const shortcut = '{Control>}{Shift>}{P}{/Control}{/Shift}'
236+
237+
it('enters preview mode when editing', async () => {
238+
const {getInput, user} = await render(<UncontrolledEditor />)
239+
await user.type(getInput(), shortcut)
240+
})
241+
242+
it('enters edit mode when previewing', async () => {
243+
const {getInput, user, getViewSwitch} = await render(<UncontrolledEditor />)
244+
await user.click(getViewSwitch())
245+
await user.keyboard(shortcut)
246+
expect(getInput()).toHaveFocus()
247+
})
248+
})
249+
234250
describe('action buttons', () => {
235251
it('renders custom action buttons', async () => {
236252
const {getActionButton} = await render(

src/drafts/MarkdownEditor/MarkdownEditor.tsx

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import {isMacOS} from '@primer/behaviors/utils'
21
import {useSSRSafeId} from '@react-aria/ssr'
32
import React, {forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react'
43
import Box from '../../Box'
@@ -24,6 +23,7 @@ import {SavedRepliesContext, SavedRepliesHandle, SavedReply} from './_SavedRepli
2423
import {Emoji} from './suggestions/_useEmojiSuggestions'
2524
import {Mentionable} from './suggestions/_useMentionSuggestions'
2625
import {Reference} from './suggestions/_useReferenceSuggestions'
26+
import {isModifierKey} from './utils'
2727

2828
export type MarkdownEditorProps = SxProp & {
2929
/** Current value of the editor as a multiline markdown string. */
@@ -119,6 +119,15 @@ const CONDENSED_WIDTH_THRESHOLD = 675
119119
const {Slot, Slots} = createSlots(['Toolbar', 'Actions', 'Label'])
120120
export const MarkdownEditorSlot = Slot
121121

122+
/**
123+
* We want to switch editors from preview mode on cmd/ctrl+shift+P. But in preview mode,
124+
* there's no input to focus so we have to bind the event to the document. If there are
125+
* multiple editors, we want the most recent one to switch to preview mode to be the one
126+
* that we switch back to edit mode, so we maintain a LIFO stack of IDs of editors in
127+
* preview mode.
128+
*/
129+
let editorsInPreviewMode: string[] = []
130+
122131
/**
123132
* Markdown textarea with controls & keyboard shortcuts.
124133
*/
@@ -245,7 +254,7 @@ const MarkdownEditor = forwardRef<MarkdownEditorHandle, MarkdownEditorProps>(
245254
savedRepliesRef.current?.openMenu()
246255
e.preventDefault()
247256
e.stopPropagation()
248-
} else if (isMacOS() ? e.metaKey : e.ctrlKey) {
257+
} else if (isModifierKey(e)) {
249258
if (e.key === 'Enter') onPrimaryAction?.()
250259
else if (e.key === 'b') format?.bold()
251260
else if (e.key === 'i') format?.italic()
@@ -255,6 +264,7 @@ const MarkdownEditor = forwardRef<MarkdownEditorHandle, MarkdownEditorProps>(
255264
else if (e.key === '8') format?.unorderedList()
256265
else if (e.shiftKey && e.key === '7') format?.orderedList()
257266
else if (e.shiftKey && e.key === 'l') format?.taskList()
267+
else if (e.shiftKey && e.key === 'p') setView?.('preview')
258268
else return
259269

260270
e.preventDefault()
@@ -266,6 +276,34 @@ const MarkdownEditor = forwardRef<MarkdownEditorHandle, MarkdownEditorProps>(
266276
}
267277
)
268278

279+
useEffect(() => {
280+
if (view === 'preview') {
281+
editorsInPreviewMode.push(id)
282+
283+
const handler = (e: KeyboardEvent) => {
284+
if (
285+
!e.defaultPrevented &&
286+
editorsInPreviewMode.at(-1) === id &&
287+
isModifierKey(e) &&
288+
e.shiftKey &&
289+
e.key === 'p'
290+
) {
291+
setView?.('edit')
292+
requestAnimationFrame(() => inputRef.current?.focus())
293+
e.preventDefault()
294+
}
295+
}
296+
document.addEventListener('keydown', handler)
297+
298+
return () => {
299+
document.removeEventListener('keydown', handler)
300+
// Performing the filtering in the cleanup callback allows it to happen also when
301+
// the user clicks the toggle button, not just on keyboard shortcut
302+
editorsInPreviewMode = editorsInPreviewMode.filter(id_ => id_ !== id)
303+
}
304+
}
305+
}, [view, setView, id])
306+
269307
// If we don't memoize the context object, every child will rerender on every render even if memoized
270308
const context = useMemo(
271309
() => ({disabled, formattingToolsRef, condensed, required}),
@@ -366,6 +404,7 @@ const MarkdownEditor = forwardRef<MarkdownEditorHandle, MarkdownEditorProps>(
366404
boxSizing: 'border-box'
367405
}}
368406
aria-live="polite"
407+
tabIndex={-1}
369408
>
370409
<h2 style={a11yOnlyStyle}>Rendered Markdown Preview</h2>
371410
<MarkdownViewer
@@ -392,6 +431,5 @@ const MarkdownEditor = forwardRef<MarkdownEditorHandle, MarkdownEditorProps>(
392431
)
393432
}
394433
)
395-
MarkdownEditor.displayName = 'MarkdownEditor'
396434

397435
export default MarkdownEditor

src/drafts/MarkdownEditor/utils.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import {isMacOS} from '@primer/behaviors/utils'
2+
13
export const getSelectedLineRange = (textarea: HTMLTextAreaElement): [number, number] => {
24
// Subtract one from the caret position so the newline found is not the one _at_ the caret position
35
// then add one because we don't want to include the found newline. Also changes -1 (not found) result to 0
@@ -16,3 +18,6 @@ export const markdownLink = (text: string, url: string) =>
1618
`[${text.replaceAll('[', '\\[').replaceAll(']', '\\]')}](${url.replaceAll('(', '\\(').replaceAll(')', '\\)')})`
1719

1820
export const markdownImage = (altText: string, url: string) => `!${markdownLink(altText, url)}`
21+
22+
export const isModifierKey = (event: KeyboardEvent | React.KeyboardEvent<unknown>) =>
23+
isMacOS() ? event.metaKey : event.ctrlKey

0 commit comments

Comments
 (0)