Skip to content

Commit

Permalink
Add MarkdownViewer stories
Browse files Browse the repository at this point in the history
  • Loading branch information
iansan5653 authored Aug 3, 2022
1 parent 3d9af5d commit 033ada5
Show file tree
Hide file tree
Showing 3 changed files with 217 additions and 54 deletions.
105 changes: 51 additions & 54 deletions src/drafts/MarkdownEditor/MarkdownEditor.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,21 @@ const savedReplies: SavedReply[] = [
}
]

const onUploadFile = async (file: File) => {
const wait = 0.0002 * file.size + 500
await delay(wait / 2)
// to demo file rejections:
if (file.name.toLowerCase().startsWith('a')) throw new Error("Rejected file for starting with the letter 'a'")
// 0.5 - 5 seconds depending on file size up to about 20 MB
await delay(wait / 2)
return {file, url: fakeFileUrl(file)}
}

const renderPreview = async () => {
await delay(500)
return 'Previewing Markdown is not supported in this example.'
}

export const Default = ({
disabled,
fullHeight,
Expand All @@ -189,15 +204,6 @@ export const Default = ({
}: ArgProps) => {
const [value, setValue] = useState('')

const onUploadFile = async (file: File) => {
const wait = 0.0002 * file.size + 500
await delay(wait / 2)
if (file.name.toLowerCase().startsWith('a')) throw new Error("Rejected file for starting with the letter 'a'")
// 0.5 - 5 seconds depending on file size up to about 20 MB
await delay(wait / 2)
return {file, url: fakeFileUrl(file)}
}

return (
<>
<MarkdownEditor
Expand All @@ -210,10 +216,7 @@ export const Default = ({
minHeightLines={minHeightLines}
maxHeightLines={maxHeightLines}
placeholder="Enter some Markdown..."
onRenderPreview={async () => {
await delay(500)
return 'Previewing Markdown is not supported in this example.'
}}
onRenderPreview={renderPreview}
onUploadFile={fileUploadsEnabled ? onUploadFile : undefined}
emojiSuggestions={emojis}
mentionSuggestions={mentionables}
Expand Down Expand Up @@ -243,49 +246,43 @@ export const CustomButtons = ({
}: ArgProps) => {
const [value, setValue] = useState('')

const onUploadFile = async (file: File) => {
// 0.5 - 5 seconds depending on file size up to about 20 MB
await delay(0.0002 * file.size + 500)
return {file, url: fakeFileUrl(file)}
}

return (
<MarkdownEditor
value={value}
onChange={setValue}
onPrimaryAction={onSubmit}
disabled={disabled}
fullHeight={fullHeight}
monospace={monospace}
minHeightLines={minHeightLines}
maxHeightLines={maxHeightLines}
placeholder="Enter some Markdown..."
onRenderPreview={async () => {
await delay(500)
return 'Previewing Markdown is not supported in this example.'
}}
onUploadFile={fileUploadsEnabled ? onUploadFile : undefined}
emojiSuggestions={emojis}
mentionSuggestions={mentionables}
referenceSuggestions={references}
required={required}
savedReplies={savedRepliesEnabled ? savedReplies : undefined}
>
<MarkdownEditor.Label visuallyHidden={hideLabel}>Markdown Editor Example</MarkdownEditor.Label>
<>
<MarkdownEditor
value={value}
onChange={setValue}
onPrimaryAction={onSubmit}
disabled={disabled}
fullHeight={fullHeight}
monospace={monospace}
minHeightLines={minHeightLines}
maxHeightLines={maxHeightLines}
placeholder="Enter some Markdown..."
onRenderPreview={renderPreview}
onUploadFile={fileUploadsEnabled ? onUploadFile : undefined}
emojiSuggestions={emojis}
mentionSuggestions={mentionables}
referenceSuggestions={references}
required={required}
savedReplies={savedRepliesEnabled ? savedReplies : undefined}
>
<MarkdownEditor.Label visuallyHidden={hideLabel}>Markdown Editor Example</MarkdownEditor.Label>

<MarkdownEditor.Toolbar>
<MarkdownEditor.ToolbarButton icon={DiffIcon} onClick={onDiffClick} aria-label="Custom Button" />
<MarkdownEditor.DefaultToolbarButtons />
</MarkdownEditor.Toolbar>
<MarkdownEditor.Toolbar>
<MarkdownEditor.ToolbarButton icon={DiffIcon} onClick={onDiffClick} aria-label="Custom Button" />
<MarkdownEditor.DefaultToolbarButtons />
</MarkdownEditor.Toolbar>

<MarkdownEditor.Actions>
<MarkdownEditor.ActionButton variant="danger" onClick={() => setValue('')}>
Reset
</MarkdownEditor.ActionButton>
<MarkdownEditor.ActionButton variant="primary" onClick={onSubmit}>
Submit
</MarkdownEditor.ActionButton>
</MarkdownEditor.Actions>
</MarkdownEditor>
<MarkdownEditor.Actions>
<MarkdownEditor.ActionButton variant="danger" onClick={() => setValue('')}>
Reset
</MarkdownEditor.ActionButton>
<MarkdownEditor.ActionButton variant="primary" onClick={onSubmit}>
Submit
</MarkdownEditor.ActionButton>
</MarkdownEditor.Actions>
</MarkdownEditor>
<p>Note: for demo purposes, files starting with &quot;A&quot; will be rejected.</p>
</>
)
}
161 changes: 161 additions & 0 deletions src/drafts/MarkdownViewer/MarkdownViewer.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
import React, {Meta} from '@storybook/react'
import {debounce} from 'lodash'
import {useCallback, useMemo, useState} from 'react'
import BaseStyles from '../../BaseStyles'
import Box from '../../Box'
import ThemeProvider from '../../ThemeProvider'
import {useSafeAsyncCallback} from '../hooks/useSafeAsyncCallback'
import MarkdownViewer from './MarkdownViewer'

const meta: Meta = {
title: 'Forms/MarkdownViewer',
decorators: [
Story => {
return (
<ThemeProvider>
<BaseStyles>
<Box sx={{maxWidth: 800}}>{Story()}</Box>
</BaseStyles>
</ThemeProvider>
)
}
],
parameters: {
controls: {
include: ['Loading', 'Open Links In New Tab']
}
},
component: MarkdownViewer,
argTypes: {
loading: {
name: 'Loading',
defaultValue: false,
control: {
type: 'boolean'
}
},
linksInNewTab: {
name: 'Open Links In New Tab',
defaultValue: false,
control: {
type: 'boolean'
}
}
}
}

export default meta

type ArgProps = {
loading: boolean
linksInNewTab: boolean
}

// This is actual output from the GitHub Markdown /preview endpoint:
const sampleHtml = `
<h3>Sample markdown</h3>
<h4>Formatted text</h4>
<ul>
<li><strong>bold</strong></li>
<li><em>italic</em></li>
<li><code class="notranslate">inline code</code></li>
</ul>
<blockquote>
<p>quote</p>
</blockquote>
<pre class="notranslate"><code class="notranslate">code block
</code></pre>
<h4>Links</h4>
<ul>
<li><a href="https://github.com">GitHub</a></li>
<li><a href="https://primer.style" rel="nofollow">Primer</a></li>
<li><a href="https://www.githubuniverse.com/" rel="nofollow">Universe</a></li>
</ul>
<h4>Tasks</h4>
<ul class="contains-task-list">
<li class="task-list-item"><input type="checkbox" id="" disabled="" class="task-list-item-checkbox"> Task 1</li>
<li class="task-list-item"><input type="checkbox" id="" disabled="" class="task-list-item-checkbox"> Task 2</li>
<li class="task-list-item"><input type="checkbox" id="" disabled="" class="task-list-item-checkbox"> Task 3</li>
</ul>`

const htmlObject = {__html: sampleHtml}

const sampleMarkdownSource = `
### Sample markdown
#### Formatted text
- **bold**
- _italic_
- \`inline code\`
> quote
\`\`\`
code block
\`\`\`
#### Links
- [GitHub](https://github.com)
- [Primer](https://primer.style)
- [Universe](https://www.githubuniverse.com/)
#### Tasks
- [ ] Task 1
- [ ] Task 2
- [ ] Task 3`

export const Default = ({loading, linksInNewTab}: ArgProps) => (
<MarkdownViewer loading={loading} openLinksInNewTab={linksInNewTab} dangerousRenderedHTML={htmlObject} />
)

export const LinkInterception = ({loading}: ArgProps) => (
<MarkdownViewer
loading={loading}
onLinkClick={event => {
event.preventDefault()
alert(`Link clicked: ${event.target instanceof HTMLAnchorElement ? event.target.href : 'unknown'}`)
}}
dangerousRenderedHTML={htmlObject}
/>
)

export const Interactive = ({loading, linksInNewTab}: ArgProps) => {
const [markdown, setMarkdown] = useState(sampleMarkdownSource)
const [disabled, setDisabled] = useState(false)

// Any state-setting inside a debounced function and/or after an async call should be done safely
// to avoid setting state after the component unmounts
const safeSetDisabled = useSafeAsyncCallback(setDisabled)

const saveChanges = useCallback(async () => {
// Disable interaction for the duration of the request to avoid conflicts
safeSetDisabled(true)
// In production this would make an API request to save the markdown and update the rendered HTML
await new Promise(r => setTimeout(r, 500))
safeSetDisabled(false)
}, [safeSetDisabled])

// saveChanges itself must also be called safely to avoid accidentally calling an outdated reference
// Important to allow calling after unmount to avoid loss of data if the component unmounts before saving
const safeSaveChanges = useSafeAsyncCallback(saveChanges, true)

// We always want to debounce the request to avoid disabling checkboxes in between every click
const debouncedSaveChanges = useMemo(() => debounce(safeSaveChanges, 1000), [safeSaveChanges])

return (
<MarkdownViewer
loading={loading}
openLinksInNewTab={linksInNewTab}
onChange={md => {
setMarkdown(md)
debouncedSaveChanges()
}}
markdownValue={markdown}
dangerousRenderedHTML={htmlObject}
disabled={disabled}
/>
)
}
5 changes: 5 additions & 0 deletions src/drafts/MarkdownViewer/MarkdownViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ export type InteractiveMarkdownViewerProps = CoreMarkdownViewerProps & {
* Called when the user interacts and updates the Markdown. The rendered Markdown is
* updated eagerly - if the request fails, a rejected Promise should be returned by
* this handler. In that case, the viewer will revert the visual change.
*
* If the change is handled by an async API request (as it typically will be in production
* code), the viewer should be `disabled` while the request is pending to avoid conflicts.
* To allow users to check multiple boxes rapidly, the API request should be debounced (an
* ideal debounce duration is about 1 second).
*/
onChange: (markdown: string) => void | Promise<void>
/** Control whether interaction is disabled. */
Expand Down

0 comments on commit 033ada5

Please sign in to comment.