Skip to content

Commit 47725a9

Browse files
iansan5653Lukeghencosiddharthkp
authored
Add MarkdownEditor and MarkdownViewer components (#2182)
* Add `useCombobox` hook, extending `@github/combobox-nav` * Add `useSyntheticChange` hook * Add `InlineAutocomplete` component * Refactor and improve comments * Remove extra type * Add story and make it work with `FormControl` * Add to main exports * Add MDX file * Remove unecessary ID on textarea in story * Remove version-lock from new dependencies * Make type of render function more specific * Add unit tests * Simplify `useCombobox` and use `navigate` to focus first item Fixes not having an `aria-activedescendant` initially defined * Fix tests by wrapping `userEvent.type` in `act` * Fix preventing blur when tabbing from loading state * Delete unused imports * Change interfaces out for object types * Add accessible live status message to describe suggestions * Dynamically assign the combobox role to avoid treating the textarea as a combobox when no suggestions are available * Shorten & revise status message * Add `MarkdownViewer` component * Move to drafts * Add markdown viewer export to drafts * Move docs to drafts * Fix import in docs * Add markdown viewer doc page * Add `useUnifiedFileSelect` hook * Add `useIgnoreKeyboardActionsWhileComposing` hook * Improve the `useCombinedRefs` hook * Make file types optional in `useUnifiedFileSelect` * Export `SyntheticChangeEmitter` * Move character coordinates calculator to utils (from `InlineAutocomplete`) * Add `useDynamicTextareaHeight` hook * Update `resizeObserver` to support other elements * Add `MarkdownEditor` component * Fix dynamic height calculation when no line-height is set * Add a story for `MarkdownEditor` * Add `MarkdownEditor/index.ts` file * Move markdown builders into utils file * Add inline suggestions to story * Update combobox-nav dependency * Add option to control whether `Tab` key inserts suggestions * Style the defaulted-to first option differently from the selected option * Improve labelling * Change 'entity' for 'mentionable' naming * Accept `ReactNode` for `label` * Fade out the hint link when disabled * Improve story * Fix lint issues in MarkdownViewer tests * Allow custom toolbar using declarative API * Fix infinite rendering bug * Assign displayNames to public components * Replace `actionButtons` prop with a slots-based API * Rename describedBy to aria-describedby * Move label to slots-based API * Add display name for label * Refactor and optimize * Add documentation for subcomponents * Make file upload support optional * Add to drafts exports Co-authored-by: Luke Ghenco <[email protected]> * Update src/MarkdownEditor/index.ts Co-authored-by: Luke Ghenco <[email protected]> * Update combobox-nav dependency * Fix isMacOS calls breaking tests * Fix toolbar button aria-labels * Add another story * Fix fallback toolbar * Add initial batch of tests * Upgrade `userEvent` to v14 * Fix Autocomplete tests * Update userEvent and fix remaining tests * Add indenting tests * Add file upload tests * Add useSafeAsyncCallback hook * Improve the `useCombinedRefs` hook * Remove unused import * Add `useCombinedRefs` to hooks index * Change createSlots to use layout effects instead of regular effects * Fix tests and lint errors * Add demo and test for file failing to upload * Add tests for previewing and fix bug with controlled view mode * Make `InputLabel` work as `legend` and have correct stricter props * Remove forwarded refs from `MarkdownEditor.Label` * Add tests for basic props and config * Add accessible labelling tests & fix tests around refs and disabling * Add tests and a story for suggestions * Add support for saved replies 🎉 * Bake suggestions filtering into the component using fuzzy matching * Update and fix unit tests * Remove unused import (fix lint error) * Add `MarkdownEditor` docs * docs: add drafts metastring * Remove `selectionVariant` from suggestions list * Add `install:docs` script * Add more examples to docs * Add more stories * Fix _another_ bug with the caret-coordinates utility and single-line inputs 🙃 * Move component & hooks to drafts folder * Move stories & tests into drafts * Remove non-null assertions in tests * Move `textarea-caret` type declaration to `@types` * Add props table * Fix TS issue * Create cuddly-bags-sort.md * Update imports * Move changes into `drafts` directory * Format * Fix lint errors * Update useListInteraction to use a tracking ref * Replace`useCombinedRefs` in `MarkdownInput` * Improve `useSafeAsyncCallback` * Add `MarkdownViewer` stories * Fix documentation * Add changeset * Fix `useIgnoreKeyboardActionsWhileComposing` tests * Fix markdown-toolbar-element initialization * Fix remaining test cases * Remove console.error * Update changeset * Move character coordinates utils to drafts Co-authored-by: Luke Ghenco <[email protected]> Co-authored-by: Siddharth Kshetrapal <[email protected]>
1 parent 788d06a commit 47725a9

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+4625
-84
lines changed

.changeset/fluffy-cycles-shave.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@primer/react": patch
3+
---
4+
5+
- Add `MarkdownEditor` and `MarkdownViewer` draft components. The `MarkdownEditor` is also known as the `CommentBox` component
6+
- Add `useUnifiedFileSelect`, `useIgnoreKeyboardInputWhileComposing`, `useDynamicTextareaHeight`, and `useSafeAsyncCallback` draft hooks

@types/fzy-js/index.d.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
declare module 'fzy.js' {
2+
// as defined by https://github.com/jhawthorn/fzy.js/blob/master/index.js#L189
3+
export const SCORE_MIN: typeof Infinity // for -Infinity
4+
export const SCORE_MAX: typeof Infinity
5+
6+
export const SCORE_GAP_LEADING: number
7+
export const SCORE_GAP_TRAILING: number
8+
export const SCORE_GAP_INNER: number
9+
export const SCORE_MATCH_CONSECUTIVE: number
10+
export const SCORE_MATCH_SLASH: number
11+
export const SCORE_MATCH_WORD: number
12+
export const SCORE_MATCH_CAPITAL: number
13+
export const SCORE_MATCH_DOT: number
14+
15+
/**
16+
* score
17+
* @param searchQuery - the user filter (the "needle")
18+
* @param text - full text of the item being matched (the "haystack")
19+
* @returns the score
20+
*/
21+
export function score(searchQuery: string, text: string): number
22+
/**
23+
* positions
24+
* @param searchQuery - the user filter (the "needle")
25+
* @param text - full text of the item being matched (the "haystack")
26+
* @returns the position for each character match in the sequence
27+
*/
28+
export function positions(searchQuery: string, text: string): Array<number>
29+
/**
30+
* hasMatch
31+
* @param searchQuery - the user filter (the "needle")
32+
* @param text - full text of the item being matched (the "haystack")
33+
* @returns whether or not there is a match in the sequence
34+
*/
35+
export function hasMatch(searchQuery: string, text: string): boolean
36+
}
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
---
2+
componentId: markdown_editor
3+
title: MarkdownEditor
4+
status: Draft
5+
description: Full-featured Markdown input.
6+
storybook: '/react/storybook?path=/story/forms-markdowneditor--default'
7+
---
8+
9+
```js
10+
import {MarkdownEditor} from '@primer/react/drafts'
11+
```
12+
13+
`MarkdownEditor` is a full-featured editor for GitHub Flavored Markdown, with support for:
14+
15+
- Formatting (keyboard shortcuts & toolbar buttons)
16+
- File uploads (drag & drop, paste, click to upload)
17+
- Inline suggestions (emojis, `@` mentions, and `#` references)
18+
- Saved replies
19+
- Markdown pasting (ie, paste URL onto selected text to create a link)
20+
- List editing (create a new list item on `Enter`)
21+
- Indenting selected text
22+
23+
## Examples
24+
25+
### Minimal Example
26+
27+
A `Label` is always required for accessibility:
28+
29+
```javascript live noinline drafts
30+
const renderMarkdown = async (markdown) => {
31+
// In production code, this would make a query to some external API endpoint to render
32+
return "Rendered Markdown."
33+
}
34+
35+
const MinimalExample = () => {
36+
const [value, setValue] = React.useState('')
37+
38+
return (
39+
<MarkdownEditor
40+
value={value}
41+
onChange={setValue}
42+
onRenderPreview={renderMarkdown}
43+
>
44+
<MarkdownEditor.Label>Minimal Example</MarkdownEditor.Label>
45+
</MarkdownEditor>
46+
)
47+
}
48+
49+
render(MinimalExample)
50+
```
51+
52+
### Suggestions, File Uploads, and Saved Replies
53+
54+
```javascript live noinline drafts
55+
const renderMarkdown = async (markdown) => "Rendered Markdown."
56+
57+
const uploadFile = async (file) => ({
58+
url: `https://example.com/${encodeURIComponent(file.name)}`,
59+
file
60+
})
61+
62+
const emojis = [
63+
{name: '+1', character: '👍'},
64+
{name: '-1', character: '👎'},
65+
{name: 'heart', character: '❤️'},
66+
{name: 'wave', character: '👋'},
67+
{name: 'raised_hands', character: '🙌'},
68+
{name: 'pray', character: '🙏'},
69+
{name: 'clap', character: '👏'},
70+
{name: 'ok_hand', character: '👌'},
71+
{name: 'point_up', character: '☝️'},
72+
{name: 'point_down', character: '👇'},
73+
{name: 'point_left', character: '👈'},
74+
{name: 'point_right', character: '👉'},
75+
{name: 'raised_hand', character: ''},
76+
{name: 'thumbsup', character: '👍'},
77+
{name: 'thumbsdown', character: '👎'}
78+
]
79+
80+
const references = [
81+
{id: '1', titleText: 'Add logging functionality', titleHtml: 'Add logging functionality'},
82+
{
83+
id: '2',
84+
titleText: 'Error: `Failed to install` when installing',
85+
titleHtml: 'Error: <code>Failed to install</code> when installing'
86+
},
87+
{id: '3', titleText: 'Add error-handling functionality', titleHtml: 'Add error-handling functionality'}
88+
]
89+
90+
const mentionables = [
91+
{identifier: 'monalisa', description: 'Monalisa Octocat'},
92+
{identifier: 'github', description: 'GitHub'},
93+
{identifier: 'primer', description: 'Primer'}
94+
]
95+
96+
const savedReplies = [
97+
{name: 'Duplicate', content: 'Duplicate of #'},
98+
{name: 'Welcome', content: 'Welcome to the project!\n\nPlease be sure to read the contributor guidelines.'},
99+
{name: 'Thanks', content: 'Thanks for your contribution!'}
100+
]
101+
102+
const MinimalExample = () => {
103+
const [value, setValue] = React.useState('')
104+
105+
return (
106+
<MarkdownEditor
107+
value={value}
108+
onChange={setValue}
109+
onRenderPreview={renderMarkdown}
110+
111+
onUploadFile={uploadFile}
112+
113+
emojiSuggestions={emojis}
114+
referenceSuggestions={references}
115+
mentionSuggestions={mentionables}
116+
117+
savedReplies={savedReplies}
118+
>
119+
<MarkdownEditor.Label>Suggestions, File Uploads, and Saved Replies Example</MarkdownEditor.Label>
120+
</MarkdownEditor>
121+
)
122+
}
123+
124+
render(MinimalExample)
125+
```
126+
127+
### Custom Buttons
128+
129+
```javascript live noinline drafts
130+
const renderMarkdown = async (markdown) => "Rendered Markdown."
131+
132+
const MinimalExample = () => {
133+
const [value, setValue] = React.useState('')
134+
135+
return (
136+
<MarkdownEditor
137+
value={value}
138+
onChange={setValue}
139+
onRenderPreview={renderMarkdown}
140+
>
141+
<MarkdownEditor.Label visuallyHidden>Custom Buttons</MarkdownEditor.Label>
142+
143+
<MarkdownEditor.Toolbar>
144+
<MarkdownEditor.ToolbarButton icon={SquirrelIcon} aria-label="Custom button 1" />
145+
<MarkdownEditor.DefaultToolbarButtons />
146+
<MarkdownEditor.ToolbarButton icon={BugIcon} aria-label="Custom button 2" />
147+
</MarkdownEditor.Toolbar>
148+
149+
<MarkdownEditor.Actions>
150+
<MarkdownEditor.ActionButton variant="danger">
151+
Cancel
152+
</MarkdownEditor.ActionButton>
153+
<MarkdownEditor.ActionButton variant="primary">
154+
Submit
155+
</MarkdownEditor.ActionButton>
156+
</MarkdownEditor.Actions>
157+
</MarkdownEditor>
158+
)
159+
}
160+
161+
render(MinimalExample)
162+
```
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
---
2+
componentId: markdown_viewer
3+
title: MarkdownViewer
4+
status: Draft
5+
description: Displays rendered Markdown and facilitates interaction.
6+
---
7+
8+
```js
9+
import {MarkdownViewer} from '@primer/react/drafts'
10+
```
11+
12+
The `MarkdownViewer` displays rendered Markdown with appropriate styling and handles interaction (link clicking and checkbox checking/unchecking) with that content.
13+
14+
## Examples
15+
16+
### Simple Example
17+
18+
```javascript live noinline drafts
19+
const MarkdownViewerExample = () => {
20+
return (
21+
// eslint-disable-next-line github/unescaped-html-literal
22+
<MarkdownViewer dangerousRenderedHtml={{__html: '<strong>Lorem ipsum</strong> dolor sit amet.'}} />
23+
)
24+
}
25+
26+
render(MarkdownViewerExample)
27+
```
28+
29+
### Link-Handling Example
30+
31+
```javascript live noinline drafts
32+
const MarkdownViewerExample = () => {
33+
return (
34+
<MarkdownViewer
35+
// eslint-disable-next-line github/unescaped-html-literal
36+
dangerousRenderedHtml={{__html: "<a href='https://example.com'>Example link</a>"}}
37+
onLinkClick={ev => console.log(ev)}
38+
/>
39+
)
40+
}
41+
42+
render(MarkdownViewerExample)
43+
```
44+
45+
### Checkbox Interaction Example
46+
47+
```javascript live noinline drafts
48+
const markdownSource = `
49+
text before list
50+
51+
- [ ] item 1
52+
- [ ] item 2
53+
54+
text after list`
55+
56+
const renderedHtml = `
57+
<p>text before list</p>
58+
<ul class='contains-task-list'>
59+
<li class='task-list-item'><input type='checkbox' class='task-list-item-checkbox' disabled/> item 1</li>
60+
<li class='task-list-item'><input type='checkbox' class='task-list-item-checkbox' disabled/> item 2</li>
61+
</ul>
62+
<p>text after list</p>`
63+
64+
const MarkdownViewerExample = () => {
65+
return (
66+
<MarkdownViewer
67+
dangerousRenderedHtml={{__html: renderedHtml}}
68+
markdownValue={markdownSource}
69+
onChange={value => console.log(value) /* save the value to the server */}
70+
disabled={false}
71+
/>
72+
)
73+
}
74+
75+
render(MarkdownViewerExample)
76+
```
77+
78+
## Status
79+
80+
<ComponentChecklist
81+
items={{
82+
propsDocumented: false,
83+
noUnnecessaryDeps: true,
84+
adaptsToThemes: true,
85+
adaptsToScreenSizes: true,
86+
fullTestCoverage: true,
87+
usedInProduction: true,
88+
usageExamplesDocumented: false,
89+
hasStorybookStories: false,
90+
designReviewed: false,
91+
a11yReviewed: false,
92+
stableApi: false,
93+
addressedApiFeedback: false,
94+
hasDesignGuidelines: false,
95+
hasFigmaComponent: false
96+
}}
97+
/>

docs/src/@primer/gatsby-theme-doctocat/nav.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,10 @@
155155
url: /drafts/Dialog
156156
- title: InlineAutocomplete
157157
url: /drafts/InlineAutocomplete
158+
- title: MarkdownEditor
159+
url: /drafts/MarkdownEditor
160+
- title: MarkdownViewer
161+
url: /drafts/MarkdownViewer
158162
- title: Deprecated
159163
children:
160164
- title: ActionList (legacy)

jest.config.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,7 @@ module.exports = {
1010
'<rootDir>/src/utils/test-helpers.tsx'
1111
],
1212
testMatch: ['<rootDir>/(src|codemods)/**/*.test.[jt]s?(x)', '!**/*.types.test.[jt]s?(x)'],
13-
transformIgnorePatterns: ['node_modules/(?!@github/combobox-nav|@koddsson/textarea-caret)']
13+
transformIgnorePatterns: [
14+
'node_modules/(?!@github/combobox-nav|@koddsson/textarea-caret|@github/markdown-toolbar-element)'
15+
]
1416
}

package-lock.json

Lines changed: 33 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)