Skip to content
Merged
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
3 changes: 3 additions & 0 deletions src/plugins/frontmatter/FrontmatterEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,9 @@ export const FrontmatterEditor = ({ yaml, onChange }: FrontmatterEditorProps) =>
<Dialog.Overlay className={styles.dialogOverlay} />
<Dialog.Content className={styles.largeDialogContent} data-editor-type="frontmatter">
<Dialog.Title className={styles.dialogTitle}>{t('frontmatterEditor.title', 'Edit document frontmatter')}</Dialog.Title>
<Dialog.Description className={styles.visuallyHidden}>
{t('frontmatterEditor.dialogDescription', 'Add, edit, or remove the key-value entries of the document frontmatter.')}
</Dialog.Description>
<form
onSubmit={(e) => {
void handleSubmit(onSubmit)(e)
Expand Down
3 changes: 3 additions & 0 deletions src/plugins/image/ImageDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ export const ImageDialog: React.FC = () => {
}}
>
<Dialog.Title>{t('uploadImage.dialogTitle', 'Upload an image')}</Dialog.Title>
<Dialog.Description className={styles.visuallyHidden}>
{t('uploadImage.dialogDescription', 'Insert an image from a URL or upload one from your device.')}
</Dialog.Description>
<form
onSubmit={async (e) => {
e.preventDefault()
Expand Down
2 changes: 2 additions & 0 deletions src/plugins/toolbar/primitives/DialogButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ export const DialogButton = React.forwardRef<
<Dialog.Portal container={editorRootElementRef?.current}>
<Dialog.Overlay className={styles.dialogOverlay} />
<Dialog.Content className={styles.dialogContent}>
<Dialog.Title className={styles.visuallyHidden}>{tooltipTitle}</Dialog.Title>
<Dialog.Description className={styles.visuallyHidden}>{dialogInputPlaceholder}</Dialog.Description>
<DialogForm
submitButtonTitle={submitButtonTitle}
autocompleteSuggestions={autocompleteSuggestions}
Expand Down
14 changes: 14 additions & 0 deletions src/styles/ui.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -627,6 +627,20 @@
padding-left: var(--spacing-2);
}

/* Keeps text available to assistive technology without rendering it on screen. */
.visuallyHidden {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
padding: 0;
overflow: hidden;
clip: rect(0, 0, 0, 0);
clip-path: inset(50%);
white-space: nowrap;
border-width: 0;
}

.dialogCloseButton {
@mixin clear-form-element;
position: absolute;
Expand Down
86 changes: 86 additions & 0 deletions src/test/dialog-a11y.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { useRealm } from '@mdxeditor/gurx'
import { fireEvent, render, screen } from '@testing-library/react'
import React from 'react'
import { describe, expect, it } from 'vitest'
import { MDXEditor, frontmatterPlugin, imagePlugin, insertFrontmatter$, toolbarPlugin } from '../'
import { InsertImage } from '../plugins/toolbar/components/InsertImage'
import { DialogButton } from '../plugins/toolbar/primitives/DialogButton'

// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
;(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true

const FrontmatterTrigger = () => {
const realm = useRealm()
return (
<button
type="button"
onClick={() => {
realm.pub(insertFrontmatter$)
}}
>
Edit frontmatter
</button>
)
}

/**
* Asserts that the open dialog with the given accessible name wires an
* `aria-describedby` to an element carrying the expected description text.
* Radix warns (and screen readers get no context) when this wiring is missing.
*/
function expectDialogDescription(name: string, description: string) {
const dialog = screen.getByRole('dialog', { name })
const describedById = dialog.getAttribute('aria-describedby')
expect(describedById).toBeTruthy()
const descriptionElement = document.getElementById(describedById!)
expect(descriptionElement).toHaveTextContent(description)
}

describe('editor dialog accessibility', () => {
it('describes the image dialog', () => {
render(<MDXEditor markdown="" plugins={[imagePlugin(), toolbarPlugin({ toolbarContents: () => <InsertImage /> })]} />)

fireEvent.click(screen.getByRole('button', { name: 'Insert image' }))

expectDialogDescription('Upload an image', 'Insert an image from a URL or upload one from your device.')
})

it('names and describes the shared toolbar dialog', () => {
render(
<MDXEditor
markdown=""
plugins={[
toolbarPlugin({
toolbarContents: () => (
<DialogButton
tooltipTitle="Insert YouTube video"
dialogInputPlaceholder="Paste URL"
submitButtonTitle="Insert video"
onSubmit={() => undefined}
buttonContent="video"
/>
)
})
]}
/>
)

fireEvent.click(screen.getByRole('button', { name: 'Insert YouTube video' }))

expectDialogDescription('Insert YouTube video', 'Paste URL')
})

it('describes the frontmatter dialog', async () => {
render(
<MDXEditor
markdown={'---\ntitle: hello\n---\n\n# body'}
plugins={[frontmatterPlugin(), toolbarPlugin({ toolbarContents: () => <FrontmatterTrigger /> })]}
/>
)

fireEvent.click(screen.getByRole('button', { name: 'Edit frontmatter' }))

await screen.findByRole('dialog', { name: 'Edit document frontmatter' })
expectDialogDescription('Edit document frontmatter', 'Add, edit, or remove the key-value entries of the document frontmatter.')
})
})