Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a prop to control URL pasting behavior in MarkdownEditor #2368

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/weak-peaches-teach.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@primer/react": minor
---

Add `pasteUrlsAsPlainText` prop to control URL pasting behavior in `MarkdownEditor`
18 changes: 9 additions & 9 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@
"dependencies": {
"@github/combobox-nav": "^2.1.5",
"@github/markdown-toolbar-element": "^2.1.0",
"@github/paste-markdown": "^1.3.1",
"@github/paste-markdown": "^1.4.0",
"@primer/behaviors": "^1.1.1",
"@primer/octicons-react": "^17.3.0",
"@primer/primitives": "7.9.0",
Expand Down
19 changes: 16 additions & 3 deletions src/drafts/MarkdownEditor/MarkdownEditor.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ const meta: Meta = {
'Hide Label',
'Required',
'Enable File Uploads',
'Enable Saved Replies'
'Enable Saved Replies',
'Enable Plain-Text URL Pasting'
]
}
},
Expand All @@ -57,6 +58,13 @@ const meta: Meta = {
type: 'boolean'
}
},
pasteUrlsAsPlainText: {
name: 'Enable Plain-Text URL Pasting',
defaultValue: false,
control: {
type: 'boolean'
}
},
minHeightLines: {
name: 'Minimum Height (Lines)',
defaultValue: 5,
Expand Down Expand Up @@ -122,6 +130,7 @@ type ArgProps = {
required: boolean
fileUploadsEnabled: boolean
savedRepliesEnabled: boolean
pasteUrlsAsPlainText: boolean
onSubmit: () => void
onDiffClick: () => void
}
Expand Down Expand Up @@ -200,7 +209,8 @@ export const Default = ({
required,
fileUploadsEnabled,
onSubmit,
savedRepliesEnabled
savedRepliesEnabled,
pasteUrlsAsPlainText
}: ArgProps) => {
const [value, setValue] = useState('')

Expand All @@ -223,6 +233,7 @@ export const Default = ({
referenceSuggestions={references}
savedReplies={savedRepliesEnabled ? savedReplies : undefined}
required={required}
pasteUrlsAsPlainText={pasteUrlsAsPlainText}
>
<MarkdownEditor.Label visuallyHidden={hideLabel}>Markdown Editor Example</MarkdownEditor.Label>
</MarkdownEditor>
Expand All @@ -242,7 +253,8 @@ export const CustomButtons = ({
fileUploadsEnabled,
onSubmit,
onDiffClick,
savedRepliesEnabled
savedRepliesEnabled,
pasteUrlsAsPlainText
}: ArgProps) => {
const [value, setValue] = useState('')

Expand All @@ -265,6 +277,7 @@ export const CustomButtons = ({
referenceSuggestions={references}
required={required}
savedReplies={savedRepliesEnabled ? savedReplies : undefined}
pasteUrlsAsPlainText={pasteUrlsAsPlainText}
>
<MarkdownEditor.Label visuallyHidden={hideLabel}>Markdown Editor Example</MarkdownEditor.Label>

Expand Down
31 changes: 31 additions & 0 deletions src/drafts/MarkdownEditor/MarkdownEditor.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {DiffAddedIcon} from '@primer/octicons-react'
import {fireEvent, render as _render, waitFor, within} from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import {UserEvent} from '@testing-library/user-event/dist/types/setup'
import React, {forwardRef, useLayoutEffect, useRef, useState} from 'react'
import MarkdownEditor, {Emoji, MarkdownEditorHandle, MarkdownEditorProps, Mentionable, Reference, SavedReply} from '.'
import ThemeProvider from '../../ThemeProvider'
Expand Down Expand Up @@ -1140,4 +1141,34 @@ describe('MarkdownEditor', () => {
)
}
})

describe('pasting URLs', () => {
const typeAndPaste = async (user: UserEvent, input: HTMLTextAreaElement) => {
await user.type(input, 'lorem ipsum dolor sit amet')
input.setSelectionRange(6, 11)

// userEvent.paste() doesn't seem to fire the `paste` event that paste-markdown listens for.
// So we simulate it. This approach is somewhat fragile because it relies on the internals
// of paste-markdown not using any other properties on the event or DataTransfer instance.
// We can't just construct a `new DataTransfer` because that's not implemented in JSDOM.
fireEvent.paste(input, {clipboardData: {types: ['text/plain'], getData: () => 'https://github.com'}})
}

const linkifiedResult = 'lorem [ipsum](https://github.com) dolor sit amet'
const plainResult = 'lorem ipsum dolor sit amet' // the real-world plain text result should have "https://github.com" instead of "ipsum", but fireEvent.paste doesn't actually update the input value

it('pastes URLs onto selected text as links by default', async () => {
const {getInput, user} = await render(<UncontrolledEditor />)
const input = getInput()
await typeAndPaste(user, input)
expect(input).toHaveValue(linkifiedResult)
})

it('pastes URLs onto selected text as plain text when `pasteUrlsAsPlainText` enabled', async () => {
const {getInput, user} = await render(<UncontrolledEditor pasteUrlsAsPlainText />)
const input = getInput()
await typeAndPaste(user, input)
expect(input).toHaveValue(plainResult)
})
})
})
12 changes: 11 additions & 1 deletion src/drafts/MarkdownEditor/MarkdownEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,14 @@ export type MarkdownEditorProps = SxProp & {
name?: string
/** To enable the saved replies feature, provide an array of replies. */
savedReplies?: SavedReply[]
/**
* Control whether URLs are pasted as plain text instead of as formatted links (if the
* user has selected some text before pasting). Defaults to `false` (URLs will paste as
* links). This should typically be controlled by user settings.
*
* Users can always toggle this behavior by holding `shift` when pasting.
*/
pasteUrlsAsPlainText?: boolean
}

const handleBrand = Symbol()
Expand Down Expand Up @@ -157,7 +165,8 @@ const MarkdownEditor = forwardRef<MarkdownEditorHandle, MarkdownEditorProps>(
required = false,
name,
children,
savedReplies
savedReplies,
pasteUrlsAsPlainText = false
},
ref
) => {
Expand Down Expand Up @@ -389,6 +398,7 @@ const MarkdownEditor = forwardRef<MarkdownEditorHandle, MarkdownEditorProps>(
monospace={monospace}
required={required}
name={name}
pasteUrlsAsPlainText={pasteUrlsAsPlainText}
{...inputCompositionProps}
{...fileHandler?.pasteTargetProps}
{...fileHandler?.dropTargetProps}
Expand Down
9 changes: 8 additions & 1 deletion src/drafts/MarkdownEditor/_MarkdownInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ interface MarkdownInputProps extends Omit<TextareaProps, 'onChange'> {
minHeightLines: number
maxHeightLines: number
monospace: boolean
pasteUrlsAsPlainText: boolean
/** Use this prop to control visibility instead of unmounting, so the undo stack and custom height are preserved. */
visible: boolean
}
Expand All @@ -47,6 +48,7 @@ export const MarkdownInput = forwardRef<HTMLTextAreaElement, MarkdownInputProps>
maxHeightLines,
visible,
monospace,
pasteUrlsAsPlainText,
...props
},
forwardedRef
Expand Down Expand Up @@ -81,7 +83,12 @@ export const MarkdownInput = forwardRef<HTMLTextAreaElement, MarkdownInputProps>
const ref = useRef<HTMLTextAreaElement>(null)
useRefObjectAsForwardedRef(forwardedRef, ref)

useEffect(() => (ref.current ? subscribeToMarkdownPasting(ref.current).unsubscribe : undefined), [])
useEffect(() => {
const subscription =
ref.current &&
subscribeToMarkdownPasting(ref.current, {defaultPlainTextPaste: {urlLinks: pasteUrlsAsPlainText}})
return subscription?.unsubscribe
}, [pasteUrlsAsPlainText])

const dynamicHeightStyles = useDynamicTextareaHeight({maxHeightLines, minHeightLines, element: ref.current, value})
const heightStyles = fullHeight ? {} : dynamicHeightStyles
Expand Down