Skip to content

Commit

Permalink
Add MarkdownEditor and MarkdownViewer components (#2182)
Browse files Browse the repository at this point in the history
* 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 <lukeghenco@github.com>

* Update src/MarkdownEditor/index.ts

Co-authored-by: Luke Ghenco <lukeghenco@github.com>

* 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 <lukeghenco@github.com>
Co-authored-by: Siddharth Kshetrapal <siddharthkp@github.com>
  • Loading branch information
3 people committed Aug 10, 2022
1 parent 788d06a commit 47725a9
Show file tree
Hide file tree
Showing 49 changed files with 4,625 additions and 84 deletions.
6 changes: 6 additions & 0 deletions .changeset/fluffy-cycles-shave.md
@@ -0,0 +1,6 @@
---
"@primer/react": patch
---

- Add `MarkdownEditor` and `MarkdownViewer` draft components. The `MarkdownEditor` is also known as the `CommentBox` component
- Add `useUnifiedFileSelect`, `useIgnoreKeyboardInputWhileComposing`, `useDynamicTextareaHeight`, and `useSafeAsyncCallback` draft hooks
36 changes: 36 additions & 0 deletions @types/fzy-js/index.d.ts
@@ -0,0 +1,36 @@
declare module 'fzy.js' {
// as defined by https://github.com/jhawthorn/fzy.js/blob/master/index.js#L189
export const SCORE_MIN: typeof Infinity // for -Infinity
export const SCORE_MAX: typeof Infinity

export const SCORE_GAP_LEADING: number
export const SCORE_GAP_TRAILING: number
export const SCORE_GAP_INNER: number
export const SCORE_MATCH_CONSECUTIVE: number
export const SCORE_MATCH_SLASH: number
export const SCORE_MATCH_WORD: number
export const SCORE_MATCH_CAPITAL: number
export const SCORE_MATCH_DOT: number

/**
* score
* @param searchQuery - the user filter (the "needle")
* @param text - full text of the item being matched (the "haystack")
* @returns the score
*/
export function score(searchQuery: string, text: string): number
/**
* positions
* @param searchQuery - the user filter (the "needle")
* @param text - full text of the item being matched (the "haystack")
* @returns the position for each character match in the sequence
*/
export function positions(searchQuery: string, text: string): Array<number>
/**
* hasMatch
* @param searchQuery - the user filter (the "needle")
* @param text - full text of the item being matched (the "haystack")
* @returns whether or not there is a match in the sequence
*/
export function hasMatch(searchQuery: string, text: string): boolean
}
162 changes: 162 additions & 0 deletions docs/content/drafts/MarkdownEditor.mdx
@@ -0,0 +1,162 @@
---
componentId: markdown_editor
title: MarkdownEditor
status: Draft
description: Full-featured Markdown input.
storybook: '/react/storybook?path=/story/forms-markdowneditor--default'
---

```js
import {MarkdownEditor} from '@primer/react/drafts'
```

`MarkdownEditor` is a full-featured editor for GitHub Flavored Markdown, with support for:

- Formatting (keyboard shortcuts & toolbar buttons)
- File uploads (drag & drop, paste, click to upload)
- Inline suggestions (emojis, `@` mentions, and `#` references)
- Saved replies
- Markdown pasting (ie, paste URL onto selected text to create a link)
- List editing (create a new list item on `Enter`)
- Indenting selected text

## Examples

### Minimal Example

A `Label` is always required for accessibility:

```javascript live noinline drafts
const renderMarkdown = async (markdown) => {
// In production code, this would make a query to some external API endpoint to render
return "Rendered Markdown."
}

const MinimalExample = () => {
const [value, setValue] = React.useState('')

return (
<MarkdownEditor
value={value}
onChange={setValue}
onRenderPreview={renderMarkdown}
>
<MarkdownEditor.Label>Minimal Example</MarkdownEditor.Label>
</MarkdownEditor>
)
}

render(MinimalExample)
```

### Suggestions, File Uploads, and Saved Replies

```javascript live noinline drafts
const renderMarkdown = async (markdown) => "Rendered Markdown."

const uploadFile = async (file) => ({
url: `https://example.com/${encodeURIComponent(file.name)}`,
file
})

const emojis = [
{name: '+1', character: '👍'},
{name: '-1', character: '👎'},
{name: 'heart', character: '❤️'},
{name: 'wave', character: '👋'},
{name: 'raised_hands', character: '🙌'},
{name: 'pray', character: '🙏'},
{name: 'clap', character: '👏'},
{name: 'ok_hand', character: '👌'},
{name: 'point_up', character: '☝️'},
{name: 'point_down', character: '👇'},
{name: 'point_left', character: '👈'},
{name: 'point_right', character: '👉'},
{name: 'raised_hand', character: ''},
{name: 'thumbsup', character: '👍'},
{name: 'thumbsdown', character: '👎'}
]

const references = [
{id: '1', titleText: 'Add logging functionality', titleHtml: 'Add logging functionality'},
{
id: '2',
titleText: 'Error: `Failed to install` when installing',
titleHtml: 'Error: <code>Failed to install</code> when installing'
},
{id: '3', titleText: 'Add error-handling functionality', titleHtml: 'Add error-handling functionality'}
]

const mentionables = [
{identifier: 'monalisa', description: 'Monalisa Octocat'},
{identifier: 'github', description: 'GitHub'},
{identifier: 'primer', description: 'Primer'}
]

const savedReplies = [
{name: 'Duplicate', content: 'Duplicate of #'},
{name: 'Welcome', content: 'Welcome to the project!\n\nPlease be sure to read the contributor guidelines.'},
{name: 'Thanks', content: 'Thanks for your contribution!'}
]

const MinimalExample = () => {
const [value, setValue] = React.useState('')

return (
<MarkdownEditor
value={value}
onChange={setValue}
onRenderPreview={renderMarkdown}

onUploadFile={uploadFile}

emojiSuggestions={emojis}
referenceSuggestions={references}
mentionSuggestions={mentionables}

savedReplies={savedReplies}
>
<MarkdownEditor.Label>Suggestions, File Uploads, and Saved Replies Example</MarkdownEditor.Label>
</MarkdownEditor>
)
}

render(MinimalExample)
```

### Custom Buttons

```javascript live noinline drafts
const renderMarkdown = async (markdown) => "Rendered Markdown."

const MinimalExample = () => {
const [value, setValue] = React.useState('')

return (
<MarkdownEditor
value={value}
onChange={setValue}
onRenderPreview={renderMarkdown}
>
<MarkdownEditor.Label visuallyHidden>Custom Buttons</MarkdownEditor.Label>

<MarkdownEditor.Toolbar>
<MarkdownEditor.ToolbarButton icon={SquirrelIcon} aria-label="Custom button 1" />
<MarkdownEditor.DefaultToolbarButtons />
<MarkdownEditor.ToolbarButton icon={BugIcon} aria-label="Custom button 2" />
</MarkdownEditor.Toolbar>

<MarkdownEditor.Actions>
<MarkdownEditor.ActionButton variant="danger">
Cancel
</MarkdownEditor.ActionButton>
<MarkdownEditor.ActionButton variant="primary">
Submit
</MarkdownEditor.ActionButton>
</MarkdownEditor.Actions>
</MarkdownEditor>
)
}

render(MinimalExample)
```
97 changes: 97 additions & 0 deletions docs/content/drafts/MarkdownViewer.mdx
@@ -0,0 +1,97 @@
---
componentId: markdown_viewer
title: MarkdownViewer
status: Draft
description: Displays rendered Markdown and facilitates interaction.
---

```js
import {MarkdownViewer} from '@primer/react/drafts'
```

The `MarkdownViewer` displays rendered Markdown with appropriate styling and handles interaction (link clicking and checkbox checking/unchecking) with that content.

## Examples

### Simple Example

```javascript live noinline drafts
const MarkdownViewerExample = () => {
return (
// eslint-disable-next-line github/unescaped-html-literal
<MarkdownViewer dangerousRenderedHtml={{__html: '<strong>Lorem ipsum</strong> dolor sit amet.'}} />
)
}

render(MarkdownViewerExample)
```

### Link-Handling Example

```javascript live noinline drafts
const MarkdownViewerExample = () => {
return (
<MarkdownViewer
// eslint-disable-next-line github/unescaped-html-literal
dangerousRenderedHtml={{__html: "<a href='https://example.com'>Example link</a>"}}
onLinkClick={ev => console.log(ev)}
/>
)
}

render(MarkdownViewerExample)
```

### Checkbox Interaction Example

```javascript live noinline drafts
const markdownSource = `
text before list
- [ ] item 1
- [ ] item 2
text after list`

const renderedHtml = `
<p>text before list</p>
<ul class='contains-task-list'>
<li class='task-list-item'><input type='checkbox' class='task-list-item-checkbox' disabled/> item 1</li>
<li class='task-list-item'><input type='checkbox' class='task-list-item-checkbox' disabled/> item 2</li>
</ul>
<p>text after list</p>`

const MarkdownViewerExample = () => {
return (
<MarkdownViewer
dangerousRenderedHtml={{__html: renderedHtml}}
markdownValue={markdownSource}
onChange={value => console.log(value) /* save the value to the server */}
disabled={false}
/>
)
}

render(MarkdownViewerExample)
```

## Status

<ComponentChecklist
items={{
propsDocumented: false,
noUnnecessaryDeps: true,
adaptsToThemes: true,
adaptsToScreenSizes: true,
fullTestCoverage: true,
usedInProduction: true,
usageExamplesDocumented: false,
hasStorybookStories: false,
designReviewed: false,
a11yReviewed: false,
stableApi: false,
addressedApiFeedback: false,
hasDesignGuidelines: false,
hasFigmaComponent: false
}}
/>
4 changes: 4 additions & 0 deletions docs/src/@primer/gatsby-theme-doctocat/nav.yml
Expand Up @@ -155,6 +155,10 @@
url: /drafts/Dialog
- title: InlineAutocomplete
url: /drafts/InlineAutocomplete
- title: MarkdownEditor
url: /drafts/MarkdownEditor
- title: MarkdownViewer
url: /drafts/MarkdownViewer
- title: Deprecated
children:
- title: ActionList (legacy)
Expand Down
4 changes: 3 additions & 1 deletion jest.config.js
Expand Up @@ -10,5 +10,7 @@ module.exports = {
'<rootDir>/src/utils/test-helpers.tsx'
],
testMatch: ['<rootDir>/(src|codemods)/**/*.test.[jt]s?(x)', '!**/*.types.test.[jt]s?(x)'],
transformIgnorePatterns: ['node_modules/(?!@github/combobox-nav|@koddsson/textarea-caret)']
transformIgnorePatterns: [
'node_modules/(?!@github/combobox-nav|@koddsson/textarea-caret|@github/markdown-toolbar-element)'
]
}
33 changes: 33 additions & 0 deletions package-lock.json

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

0 comments on commit 47725a9

Please sign in to comment.