Skip to content
Closed
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
7 changes: 0 additions & 7 deletions .changeset/metal-hornets-appear.md

This file was deleted.

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

import {Spinner, Box, Text} from '../..'
import {Spinner, LinkButton, 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, previewMode} = useContext(MarkdownEditorContext)
const {fileUploadProgress, errorMessage, previewMode} = useContext(MarkdownEditorContext)

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

return uploadButtonProps ? <FileUploadButton fileDraggedOver={fileDraggedOver} {...uploadButtonProps} /> : null
return (
<>
<MarkdownSupportedHint />

{uploadButtonProps && (
<>
<VisualSeparator />
<FileUploadButton fileDraggedOver={fileDraggedOver} {...uploadButtonProps} />
</>
)}
</>
)
})
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={PaperclipIcon}
leadingVisual={ImageIcon}
size="small"
sx={{color: 'fg.muted', fontWeight: 'normal', px: 2}}
sx={{color: 'fg.default', fontWeight: fileDraggedOver ? 'bold' : 'normal', px: 2}}
onMouseDown={(e: React.MouseEvent) => {
// Prevent pulling focus from the textarea
e.preventDefault()
Expand All @@ -73,3 +95,29 @@ 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: 15 additions & 5 deletions src/drafts/MarkdownEditor/MarkdownEditor.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,12 @@ 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('tab', {name: 'Preview'}) || result.queryByRole('tab', {name: 'Edit'})
const button = result.queryByRole('button', {name: 'Preview'}) || result.queryByRole('button', {name: 'Edit'})
if (!button) throw new Error('View switch button not found')
return button
}
Expand Down Expand Up @@ -95,6 +97,7 @@ const render = async (ui: React.ReactElement) => {
user,
queryForUploadButton,
getFooter,
getDefaultFooterButton,
getViewSwitch,
getPreview,
queryForPreview,
Expand Down Expand Up @@ -295,8 +298,13 @@ 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} = await render(
const {getActionButton, getDefaultFooterButton} = await render(
<UncontrolledEditor>
<MarkdownEditor.Footer>
<MarkdownEditor.FooterButton>Footer A</MarkdownEditor.FooterButton>
Expand All @@ -307,11 +315,12 @@ 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} = await render(
const {getActionButton, getDefaultFooterButton} = await render(
<UncontrolledEditor disabled>
<MarkdownEditor.Footer>
<MarkdownEditor.FooterButton>Footer A</MarkdownEditor.FooterButton>
Expand All @@ -323,6 +332,7 @@ 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 @@ -700,7 +710,7 @@ describe('MarkdownEditor', () => {

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

await expectFilesToBeAdded(onChange, fileB)

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

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