Skip to content
Draft
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
103 changes: 103 additions & 0 deletions .github/workflows/a11y.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
name: Accessibility

on:
push:
branches: [main]
pull_request:
types: [opened, synchronize, reopened]
workflow_dispatch:

permissions:
contents: read

env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true

concurrency:
group: a11y-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
browser:
name: Accessibility - browser
runs-on: ubuntu-latest
timeout-minutes: 30
# Match the local Docker environment and the installed Playwright version.
container:
image: mcr.microsoft.com/playwright:v1.60.0-noble
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- uses: voidzero-dev/setup-vp@3754dd7dbdb32bd8f6d28b6043de13ad3a75f21f # v1.21.1
with:
node-version-file: ".node-version"
cache: true

- name: Install dependencies
run: vp install --frozen-lockfile

# Already inside Docker; call Playwright directly to avoid nested Docker
# and task caching. Source aliases mean no package build is needed.
- name: Run axe, keyboard and visual checks
working-directory: tests
run: pnpm exec playwright test --config a11y/playwright.config.ts

- name: Upload browser report and diagnostics
if: ${{ !cancelled() }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: a11y-browser
path: |
tests/playwright-report/a11y/browser/
tests/test-results/a11y/browser/
retention-days: 7

voiceover:
name: Accessibility - VoiceOver
# Pin the OS major version; hosted images still receive patch updates.
# The initial transcripts came from macOS 27. Review any CI differences
# against these baselines rather than automatically updating snapshots.
runs-on: macos-26
timeout-minutes: 45
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- uses: voidzero-dev/setup-vp@3754dd7dbdb32bd8f6d28b6043de13ad3a75f21f # v1.21.1
with:
node-version-file: ".node-version"
cache: true

- name: Install dependencies
run: vp install --frozen-lockfile

- name: Install WebKit
working-directory: tests
run: pnpm exec playwright install webkit

- name: Configure VoiceOver
working-directory: tests
run: |
pnpm dlx @guidepup/setup@0.29.1 setup --ci
pnpm dlx @guidepup/setup@0.29.1 install

- name: Record runner version
run: sw_vers

# Invoke the runner directly so a cached task can never skip VoiceOver.
# The Playwright config restricts screen reader automation to one worker.
- name: Run screen reader checks
run: pnpm exec node tests/a11y/run.mjs --screen-reader

- name: Upload VoiceOver report and diagnostics
if: ${{ !cancelled() }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: a11y-voiceover
path: |
tests/playwright-report/a11y/voiceover/
tests/test-results/a11y/voiceover/
retention-days: 7
61 changes: 61 additions & 0 deletions a11y-study/outline-of-concerns.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
This document covers the main areas that are most relevant to the editor regarding accessibility. These areas are derived from:

- The list of issues found by the Docs team in an accessibility audit (marked with the a11y tag on GitHub).
- Scanning through the WCAG 2.0–2.2 guidelines and identifying likely problem areas.
- Long-standing UX issues that relate to accessibility but haven't yet been formally documented.

# Legibility

Sight-impaired users need to be able to read text in the editor body as well as in interactive elements like menus and toolbars. The following concerns are most relevant to BlockNote for accessibility.

## Contrast

WCAG sets guidelines for [minimum](https://www.w3.org/WAI/WCAG22/Understanding/contrast-minimum) and [enhanced](https://www.w3.org/WAI/WCAG22/Understanding/contrast-enhanced) contrast that text has to have against its background to be legible.

## Text Spacing & Resizing

Vision-impaired users have tools available, which make text on a page easier to read by applying additional CSS styles. There are WCAG guidelines that state the upper limits of how the text's [size](https://www.w3.org/WAI/WCAG21/Understanding/resize-text.html) and [spacing](https://www.w3.org/WAI/WCAG21/Understanding/text-spacing.html) may be increased, without causing content to clip.

# Keyboard & Focus Handling

Users with impaired motor control will use a keyboard or other device that relies solely on buttons to navigate a page. When it comes to BlockNote, there are a few specific cases
that we need to consider.

## Editor Focus

Currently, the editor can be pretty annoying when moving focus around a page. Once focus lands on the editor, it immediately snaps to the editor's selection. This is fine for when the user wants to focus the editor, but less pretty annoying when moving around the page and having to focus the editor due to tab order. Since it has its own tab handling, trying to move focus outside it can be frustrating.

## Block Selection

There is a distinction between selecting all content within a block and selecting the entire block, especially for things like backspace handling. This distinction is currently unclear to the user, selected blocks are not always clearly highlighted, and it's only possible to select an entire block with text content using Cmd+Click.

## Interactive Block Elements

Some blocks contain interactive elements within them, e.g., checkboxes in check list items. These are not accessible while the editor is focused and are otherwise, but their tab order is not at all intuitive.

## UI Elements

Some work has been put into making the formatting and link toolbars accessible. Suggestion menus are also keyboard-accessible. For other UI elements, keyboard navigation is either completely broken (e.g., side menu) or not considered (e.g., file panel).

## Hover Controls

Any controls that are exposed by hovering an element with the mouse cursor or focusing it must be accessible with the keyboard, as per this [WCAG guideline](https://www.w3.org/WAI/WCAG22/Understanding/content-on-hover-or-focus.html).

# Screen Reader Announcements

When screen readers announce content within the editor, they must also announce additional semantic information that would be useful when viewing and editing a document.

## Screen Reader Focus

Screen readers have their own keyboard navigation and target handling separate from the browser. This means that while an element is focused in the browser, the user can still move the screen reader to target other elements on the page. However, moving around the editor using the keyboard moves the screen reader target with it, so generally everything should work out-of-the-box in our case.

## Markup

The editor contains a lot of markup that is currently not announced by screen readers but is required for the user to understand the document structure. This is generally fixed using ARIA attributes and includes things like:

- Block type
- Block nesting
- Block children
- Block colors
- Table row/column information
- ...
38 changes: 38 additions & 0 deletions a11y-study/testing-requirements.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
This document covers what testing infrastructure must be in place to ensure that BlockNote has a good level of accessibility. This means coverage of all areas discussed in Outline of Concerns.

# Legibility

## Contrast

Contrast requirements can be checked using static DOM analysis tools like [axe-core](https://github.com/dequelabs/axe-core/tree/develop). A rule like [color-contrast](https://dequeuniversity.com/rules/axe/4.13/color-contrast?application=RuleDescription) calculates contrast between the CSS text and background colors for each element. It cannot check contrast for image, gradient, or other non-single-color backgrounds, but these aren't used in BlockNote, so it doesn't matter in our case. Visual regression testing is not necessary for checking contrast.

## Text Spacing & Resizing

The WCAG guidelines require that text spacing & sizing must be modifiable, and this is done in accessibility tools by overwriting CSS. Testing this is twofold. First, static DOM analysis using [axe-core](https://github.com/dequelabs/axe-core/tree/develop) ensures that no inline styles prevent CSS rules from applying. Visual regression testing is then used to capture screenshots and verify that content is not clipped, which is already part of our existing test infrastructure.

# Keyboard & Focus Handling

We already have keyboard handling as part of our existing tests. This is done in two ways:

1. Simulating key presses using synthetic events or directly calling their related handlers in a jsdom environment.
2. Driving the keyboard to dispatch real events in a real browser environment.

For accessibility purposes, we should be using a real browser environment. Simulated key presses in a jsdom environment are not guaranteed to behave the same as a real browser, while calling the handlers is not functionally different to having a unit test for that handler. We should refactor our existing jsdom tests to either unit or browser tests.

The scope of keyboard handling should include:

- Keyboard navigation through a page with interactive elements, including a BlockNote editor.
- Selections within an editor at different levels:
- Selecting content within blocks.
- Selecting entire blocks.
- Selecting the entire editor.
- Focusing all interactive elements within default blocks, including media controls.
- Focusing/triggering all interactive elements in the BlockNote UI, including those gated behind hovering/focusing another element.

Visual regression testing should also be used to ensure the focused elements are visually distinct, e.g., using a focus ring.

# Screen Reader Announcements

Static DOM analysis tools like [axe-core](https://github.com/dequelabs/axe-core/tree/develop) can catch a large chunk of missing screen reader announcements through the presence of things like ARIA attributes. However, they are geared more towards static websites, and we are in a fairly unique position where the user is editing content as well as viewing it, and the content itself has significantly more markup than the average website. For example, our schema allows for block indentation. It's important that the indentation level of a block is communicated by a screen reader, but it's not something that static DOM analysis tools can catch.

Therefore, we should also incorporate automated screen reader testing, which lets you capture the output of a screen reader as a page is being navigated. This lets us ensure that BlockNote-specific semantics are captured by a screen reader. While screen readers have their own keyboard navigation and targeting, we don't need to test this. When navigating through the editor using a keyboard, the screen reader target will move with the selection. Advanced screen reader navigation, like [VoiceOver's rotor](https://support.apple.com/en-euro/guide/voiceover/mchlp2719/mac), is reliant on proper DOM element semantics and ARIA attributes, so it's tested implicitly by static DOM analysis tools.
53 changes: 53 additions & 0 deletions a11y-study/tooling-and-organization.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
This document goes over the tooling we need based onTesting Requirements and discusses how accessibility tests should be organized.

# Tooling Overview

This section goes over all the relevant tooling for accessibility testing, both manual and automated.

## Linting

[oxlint](https://oxc.rs/docs/guide/usage/linter.html) includes a ruleset for accessibility linting - [jsx-a11y](https://oxc.rs/docs/guide/usage/linter/rules.html?sort=source&dir=asc&scope=jsx_a11y). This is useful for catching any obvious issues like missing labels and positive tab indices before running any automated tests.

## Static DOM Analysis

[axe-core](https://github.com/dequelabs/axe-core) is the gold standard for basic accessibility testing. It scans the DOM of a page and checks for issues similar issues to the aforementioned linter. The draw is that it has a [browser extension](https://chromewebstore.google.com/detail/axe-devtools-web-accessib/lhdoppojpmngadmnindnejefpokejbdd), and more importantly, has a [Playwright integration](https://playwright.dev/docs/accessibility-testing) which makes it much easier to slot in to our existing testing infrastructure, and scan different UI menus/toolbars with it.

## Visual Regression Testing

We can already do visual regression testing using Playwright.

## Screen Reader Automated Testing

This is something that has only come around in recent years, but it's pretty self explanatory. You can programmatically control a screen reader and transcribe its output into snapshots that you compare other test runs against.

In our case, we mostly just care about the transcriptions as the screen reader will follow the selection when navigating the editor using a keyboard.

The utility of these tests really is in looking at a transcription, comparing it to the editor state and seeing if there's any useful information that isn't being conveyed, so we can make further improvements. The "north star" is that the transcription of a screen reader provides all the necessary information for someone to be able to recreate the document, as well as keyboard inputs made by the user, without any errors.

[GuidePup](https://www.guidepup.dev/) is a library for automated screen reader testing that fits quite well into our existing stack as it integrates with Playwright. It supports [VoiceOver](https://support.apple.com/en-euro/guide/voiceover/welcome/mac) on macOS, [NVDA](https://www.nvaccess.org/) on Windows, and a virtual screen reader for use outside a browser environment. Other popular solutions include [BrowserStack](https://www.browserstack.com/docs/app-accessibility/screen-reader-automation) and [Assistiv Labs](https://assistivlabs.com/articles/automating-screen-readers-for-accessibility-testing), but these are entire platforms that we probably don't want to deal with as part of our CI. The [AT Driver](https://github.com/w3c/at-driver) is also relevant here, but is still WIP.

## Built-In Tools

Chrome has an accessibility tree viewer which is quite useful for getting item ordering with tab & screen reader navigation.

[VoiceOver](https://support.apple.com/en-euro/guide/voiceover/welcome/mac) is the built-in macOS/iOS screen reader and is helpful for manual testing. It's useful to get comfortable using it to put yourself in the user's shoes and spot issues. Windows has [NVDA](https://www.nvaccess.org/) and Android has [TalkBack](https://support.google.com/accessibility/android/answer/6283677?hl=en) as equivalents, but I haven't tried them yet.

# Automated Testing Organization

Based on the need 3 types of automated tests to cover accessibility:

1. Static DOM analysis
2. Visual regression snapshotting
3. Screen reader

TODO

## Out of Scope

### Drag & Drop

There is a WCAG [guideline on drag & drop](https://www.w3.org/WAI/WCAG22/Understanding/dragging-movements.html) which states that actions performed by drag & drop must have an alternative way of triggering them using regular clicks. We do technically have a way of doing this with keyboard shortcuts to move blocks up/down, but this doesn't fit the guideline as it uses keyboard shortcuts, not clicks. I'm not sure yet what the best UX pattern to solve this issue would be yet and think this requires a separate look.

### Announcements

TODO
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@
"e2e": "bash tests/docker-run.sh -e CI=1 -- --run",
"e2e:updateSnaps": "bash tests/docker-run.sh -e CI=1 -- --run --update=true",
"e2e:report": "serve -l 4173 tests/playwright-report",
"a11y": "node tests/a11y/run.mjs",
"a11y:screen-reader": "node tests/a11y/run.mjs --screen-reader",
"lint": "vp lint --type-aware",
"typecheck": "tsc --noEmit -p tsconfig.json",
"postpublish": "rm -rf packages/core/README.md && rm -rf packages/react/README.md",
Expand Down
12 changes: 12 additions & 0 deletions packages/core/src/blocks/ListItem/CheckListItem/block.a11y.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { test } from "../../../../../../tests/a11y/test.js";

test.fixme("check list item: checkbox is keyboard accessible while the editor has focus", async () => {
// TODO: The keyboard interaction for controls within blocks is still TBD.
// Once defined, verify reaching the checkbox while focus is within the
// editor, toggling it on/off without changing neighbouring items, and
// returning to editing. Outside the editor, skip it in page tab navigation.
// Capture the focus indicator and announcements of its name, role and state.
// A direct toggle shortcut alone would not require a full browser test;
// this placeholder is for the eventual keyboard focus/navigation behavior,
// which will also apply to other interactive blocks such as toggle headings.
});
3 changes: 2 additions & 1 deletion packages/core/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,6 @@
"skipLibCheck": true,
"noErrorTruncation": true
},
"include": ["src"]
"include": ["src"],
"exclude": ["src/**/*.a11y.spec.ts"]
}
6 changes: 5 additions & 1 deletion packages/core/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,11 @@ export default defineConfig({
setupFiles: ["./vitestSetup.ts"],
// `.browser.test` files need a real browser; the tests package's browser
// suite runs them.
exclude: [...configDefaults.exclude, "**/*.browser.test.*"],
exclude: [
...configDefaults.exclude,
"**/*.browser.test.*",
"**/*.a11y.spec.ts",
],
},
plugins: [webpackStats()],
build: {
Expand Down
Loading
Loading