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

MarkdownEditor polish #3667

Merged
merged 13 commits into from
Nov 3, 2023
7 changes: 7 additions & 0 deletions .changeset/metal-hornets-appear.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@primer/react": patch
---

Changes visual appearance of MarkdownEditor

<!-- Changed components: MarkdownEditor -->
2 changes: 1 addition & 1 deletion src/drafts/MarkdownEditor/Actions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@ Actions.displayName = 'MarkdownEditor.Actions'

export const ActionButton = forwardRef<HTMLButtonElement, ButtonProps>((props, ref) => {
const {disabled} = useContext(MarkdownEditorContext)
return <Button ref={ref} size="small" disabled={disabled} {...props} />
return <Button ref={ref} disabled={disabled} {...props} />
})
ActionButton.displayName = 'MarkdownEditor.ActionButton'
78 changes: 15 additions & 63 deletions src/drafts/MarkdownEditor/Footer.tsx
Original file line number Diff line number Diff line change
@@ -1,38 +1,36 @@
import React, {memo, forwardRef, useContext} from 'react'
import {AlertIcon, ImageIcon, MarkdownIcon} from '@primer/octicons-react'
import {PaperclipIcon} from '@primer/octicons-react'

import {Spinner, LinkButton, Box, Text} from '../..'
import {Spinner, Box, Text} from '../..'
import {Button, ButtonProps} from '../../Button'
import {MarkdownEditorContext} from './_MarkdownEditorContext'
import {useSlots} from '../../hooks/useSlots'

const uploadingNote = ([current, total]: [number, number]) =>
total === 1 ? `Uploading your file...` : `Uploading your files... (${current}/${total})`
total === 1 ? `Uploading your file` : `Uploading your files (${current}/${total})`

export const CoreFooter = ({children}: {children: React.ReactNode}) => {
const [slots, childrenWithoutSlots] = useSlots(children, {
footerButtons: FooterButton,
})

const {fileUploadProgress, errorMessage, previewMode} = useContext(MarkdownEditorContext)
const {fileUploadProgress, previewMode} = useContext(MarkdownEditorContext)

return (
<Box sx={{pt: 2, display: 'flex', gap: 2, justifyContent: 'space-between', minHeight: '36px'}} as="footer">
<Box
sx={{pt: 2, display: 'flex', gap: 2, justifyContent: 'space-between', alignItems: 'center', minHeight: '36px'}}
as="footer"
>
<Box sx={{display: 'flex', gap: 1, alignItems: 'center', fontSize: 0}}>
{previewMode ? (
<></>
) : fileUploadProgress ? (
<Text sx={{py: 1, px: 2}}>
<Text sx={{py: 1, px: 2, color: 'fg.muted'}}>
<Spinner size="small" sx={{mr: 1, verticalAlign: 'text-bottom'}} /> {uploadingNote(fileUploadProgress)}
</Text>
) : errorMessage ? (
<ErrorMessage message={errorMessage} />
) : (
<>
{slots.footerButtons && <Box sx={{display: 'flex', gap: 2}}>{slots.footerButtons}</Box>}
<DefaultFooterButtons />
</>
)}
) : null}
{slots.footerButtons && <Box sx={{display: 'flex', gap: 2}}>{slots.footerButtons}</Box>}
<DefaultFooterButtons />
</Box>
{!fileUploadProgress && <Box sx={{display: 'flex', gap: 2}}>{childrenWithoutSlots}</Box>}
</Box>
Expand All @@ -51,39 +49,19 @@ FooterButton.displayName = 'MarkdownEditor.FooterButton'
const DefaultFooterButtons = memo(() => {
const {uploadButtonProps, fileDraggedOver} = useContext(MarkdownEditorContext)

return (
<>
<MarkdownSupportedHint />

{uploadButtonProps && (
<>
<VisualSeparator />
<FileUploadButton fileDraggedOver={fileDraggedOver} {...uploadButtonProps} />
</>
)}
</>
)
return uploadButtonProps ? <FileUploadButton fileDraggedOver={fileDraggedOver} {...uploadButtonProps} /> : null
})
DefaultFooterButtons.displayName = 'MarkdownEditor.DefaultFooterButtons'

const ErrorMessage = memo(({message}: {message: string}) => (
<Text sx={{py: 1, px: 2, color: 'danger.fg'}} aria-live="polite">
<Text sx={{mr: 1}}>
<AlertIcon size="small" />
</Text>{' '}
{message}
</Text>
))

const FileUploadButton = memo(({fileDraggedOver, ...props}: Partial<ButtonProps> & {fileDraggedOver: boolean}) => {
const {condensed, disabled} = useContext(MarkdownEditorContext)

return (
<Button
variant="invisible"
leadingVisual={ImageIcon}
leadingVisual={PaperclipIcon}
size="small"
sx={{color: 'fg.default', fontWeight: fileDraggedOver ? 'bold' : 'normal', px: 2}}
sx={{color: 'fg.muted', fontWeight: 'normal', px: 2}}
onMouseDown={(e: React.MouseEvent) => {
// Prevent pulling focus from the textarea
e.preventDefault()
Expand All @@ -95,29 +73,3 @@ const FileUploadButton = memo(({fileDraggedOver, ...props}: Partial<ButtonProps>
</Button>
)
})

const VisualSeparator = memo(() => (
<Box sx={{borderRightStyle: 'solid', borderRightWidth: 1, borderRightColor: 'border.muted', height: '100%'}} />
))

const MarkdownSupportedHint = memo(() => {
const {condensed} = useContext(MarkdownEditorContext)

return (
<LinkButton
leadingVisual={MarkdownIcon}
variant="invisible"
size="small"
sx={{color: 'inherit', fontWeight: 'normal', px: 2}}
href="https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax"
target="_blank"
// The markdown editor aria-description already describes it as Markdown editor, so it's
// redundant to say Markdown is supported again here. However for sighted users, they
// cannot see the aria-description so this is a useful hint for them. So the aria-label
// is different from the visible text content.
aria-label="Markdown documentation"
>
{!condensed && <Text aria-hidden>Markdown is supported</Text>}
</LinkButton>
)
})
20 changes: 5 additions & 15 deletions src/drafts/MarkdownEditor/MarkdownEditor.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,10 @@ const render = async (ui: React.ReactElement) => {

const queryForToolbarButton = (label: string) => within(getToolbar()).queryByRole('button', {name: label})

const getDefaultFooterButton = () => within(getFooter()).getByRole('link', {name: 'Markdown documentation'})

const getActionButton = (label: string) => within(getFooter()).getByRole('button', {name: label})

const getViewSwitch = () => {
const button = result.queryByRole('button', {name: 'Preview'}) || result.queryByRole('button', {name: 'Edit'})
const button = result.queryByRole('tab', {name: 'Preview'}) || result.queryByRole('tab', {name: 'Edit'})
if (!button) throw new Error('View switch button not found')
return button
}
Expand Down Expand Up @@ -97,7 +95,6 @@ const render = async (ui: React.ReactElement) => {
user,
queryForUploadButton,
getFooter,
getDefaultFooterButton,
getViewSwitch,
getPreview,
queryForPreview,
Expand Down Expand Up @@ -298,13 +295,8 @@ describe('MarkdownEditor', () => {
})

describe('footer', () => {
it('renders default when not using custom footer', async () => {
const {getDefaultFooterButton} = await render(<UncontrolledEditor></UncontrolledEditor>)
expect(getDefaultFooterButton()).toBeInTheDocument()
})

it('renders custom buttons', async () => {
const {getActionButton, getDefaultFooterButton} = await render(
const {getActionButton} = await render(
<UncontrolledEditor>
<MarkdownEditor.Footer>
<MarkdownEditor.FooterButton>Footer A</MarkdownEditor.FooterButton>
Expand All @@ -315,12 +307,11 @@ describe('MarkdownEditor', () => {
</UncontrolledEditor>,
)
expect(getActionButton('Footer A')).toBeInTheDocument()
expect(getDefaultFooterButton()).toBeInTheDocument()
expect(getActionButton('Action A')).toBeInTheDocument()
})

it('disables buttons when the editor is disabled (unless explicitly overridden)', async () => {
const {getActionButton, getDefaultFooterButton} = await render(
const {getActionButton} = await render(
<UncontrolledEditor disabled>
<MarkdownEditor.Footer>
<MarkdownEditor.FooterButton>Footer A</MarkdownEditor.FooterButton>
Expand All @@ -332,7 +323,6 @@ describe('MarkdownEditor', () => {
</UncontrolledEditor>,
)
expect(getActionButton('Footer A')).toBeDisabled()
expect(getDefaultFooterButton()).not.toBeDisabled()
expect(getActionButton('Action A')).toBeDisabled()
expect(getActionButton('Action B')).not.toBeDisabled()
})
Expand Down Expand Up @@ -710,7 +700,7 @@ describe('MarkdownEditor', () => {

it('rejects disallows file types while accepting allowed ones', async () => {
const onChange = jest.fn()
const {getInput, getFooter} = await render(
const {getInput, getEditorContainer} = await render(
<UncontrolledEditor onUploadFile={mockUploadFile} onChange={onChange} acceptedFileTypes={['image/*']} />,
)
const input = getInput()
Expand All @@ -725,7 +715,7 @@ describe('MarkdownEditor', () => {

await expectFilesToBeAdded(onChange, fileB)

expect(getFooter()).toHaveTextContent('File type not allowed: .app')
expect(getEditorContainer()).toHaveTextContent('File type not allowed: .app')
})

it('inserts "failed to upload" note on failure', async () => {
Expand Down
Loading
Loading