Repository navigation
[COMMS-892] Improve accessibility (keyboard navigation & screen reader support) for WPs - #26027
Conversation
There was a problem hiding this comment.
Thanks a lot for taking this on, I don't think it is an easy task.
So, I tried to use this (in chrome and firefox) and run into a few unexpected behaviours:
- I can't select inline work package links with the arrow keys only. I can select them by shift + arrow key, but I believe it was like that before the change
- I can select card work packages by arrow keys, but
2. a.I need a second key press and then they get an additonal blue outline (which we shouldn't have)
2. b.I can't open the menu from there with the keyboard. I tried enter, space and tab.
Screencast.from.2026-10-20.11-35-12.mp4
- I wonder if we need some kind of eslint-plugin to ensure accessibility is good on what we have build and will build in the future. Can you please check if you find a plugin that would suit our needs?
6bb42ee to
0076a97
Compare
|
Caution The Enterprise plan field is not set on the work package Details:
Please make sure that:
|
|
Warning Flaky specs
🤖 Ask Copilot to investigateCopy the prompt below into a new comment on this PR to delegate the investigation to GitHub Copilot. It will look into the flakiness and open a separate pull request with you as reviewer. |
|
Hey @judithroth, thanks a lot for the review! About the arrow keys not selecting a work package with a single press, that's strange, because it works for me in both Chrome and Firefox. The same goes for Enter and Space opening the menu, and Tab closing it again. Could you please try once more after deleting the Screencast.from.2026-10-20.16-33-42.mp4About the third point, While testing, I also found a bug with the focus ring inside the options menu itself, and fixed it in the last commit. |
3a75e74 to
e43cb62
Compare
judithroth
left a comment
There was a problem hiding this comment.
Thanks a lot for rebasing this 💙
Some findings:
-
I would have expected to be able to use the arrow up / down keys to open the sizing dropdown:
https://github.com/user-attachments/assets/a8938bde-3145-4748-aea0-478ed8c81e3e -
My screenreader (the default of Ubuntu) also reads the status for the "compact" size, which does not show the status on the page.
-
(According to Claude): Announcements can cancel each other (useSelectionAnnouncement.ts, workPackageAccessibilityExtension.ts)
- Every work package subscribes to onSelectionChange separately, and they all share one clearAnnouncement(). That
function cancels any pending requestAnimationFrame and empties the live region. - Say the selection moves straight from work package A to B, for example ArrowDown between two adjacent block cards,
or a click. If B's listener runs before A's, B queues its announcement and A's "no longer selected" branch then
cancels it. Nothing is announced. - Which listener runs first depends on subscription order. That changes whenever the effect re-subscribes because
message changed (the work package loaded, or a collaborator changed its status). - Fix: only clear if the live region still shows this component's message, or don't cancel the pending frame in the
deselect path. - No test covers selecting one work package directly after another.
Overall: Very nice work 👏
| 'changeSize': 'Change size', | ||
| 'changeSizeAriaLabel': 'Change size: {{size}}', | ||
| 'menuAriaLabel': 'Options for work package {{id}}', | ||
| 'selectedAnnouncement': '{{description}}. Press Enter to open its options.', |
|
|
||
| it('opens the options popover from the id with the keyboard', async () => { | ||
| await renderChip(); | ||
| it('opens the options popover from the keyboard once the chip is selected', async () => { |
There was a problem hiding this comment.
👍 Thanks for adding this test. I am not sure if it belongs in this file, though?
| parseWorkPackageBlockExternalHTML, | ||
| } from './externalHtml'; | ||
| import { PasteWorkPackageLinkExtension } from '../../plugins/pasteWorkPackageLinkExtension'; | ||
| import { WorkPackageAccessibilityExtension } from '../../plugins/workPackageAccessibilityExtension'; |
There was a problem hiding this comment.
This should also be imported on the inline work package link spec.
I am aware that it most likely is ignored, but once that is fixed, it should be there so if someone only uses inline work package links, but not the block ones, it should still work.
It seems like a fix for that is already merged on BlockNote side (but not yet released)
e43cb62 to
0c90fd5
Compare
Ticket
https://community.openproject.org/projects/COMMS/work_packages/COMMS-892
Moved from opf/op-blocknote-extensions#264, where the earlier review discussion lives.
What are you trying to accomplish?
Work packages in the editor were effectively mouse-only. Inline work packages and block cards could not be operated from the keyboard, and screen readers got an unlabelled or flattened button with no idea what it was. This makes both usable without a mouse and understandable with a screen reader, and makes inline work packages and block cards behave the same way.
I validated it manually with Orca and I'm happy with how it reads. Selecting a work package announces what it is (ID, type, status, subject), the identifier is read as a menu button, the menu, its items and the current size are announced properly, and focus returns to the editor after closing.
What approach did you choose and why?
Menu button on the identifier, no tab stops. The #ID of an inline work package or card is the menu button (
aria-haspopup,aria-expanded), not the whole element, because the subject inside is a link and a button must not contain a link. Work packages are deliberately left out of the tab order, since a tab stop inside the contenteditable would pull focus and the caret out of ProseMirror. Keyboard users go through the editor selection instead. The arrow keys select the work package, Enter or Space open its menu, and Escape or Tab close it and hand focus back to the editor with the work package still selected. This also covers a menu opened with the mouse, so Tab no longer nests the block or jumps to the subject link.Announcement through a live region. A ProseMirror node selection tells a screen reader nothing, so the selected work package is announced explicitly.
Own focus styles for the menu. OpenProject loads its stylesheet into the editor's shadow root, where its focus outline resolves to nothing, so the menu items now draw their own focus ring.
The keyboard support ships with the block spec. BlockNote's inline content specs take no extensions, so the accessibility extension is registered through
openProjectWorkPackageBlockSpec, asPasteWorkPackageLinkExtensionalready is. Editors that register only the inline spec keep the roles and mouse behaviour but get no keyboard selection or announcements. The README and OpenProject register both specs.prosemirror-stateis now external and a peer dependency. It used to be bundled, so the package had its own copy andinstanceof NodeSelectionwas always false in OpenProject, while tests and the dev page passed. It now uses the host's copy, the same one BlockNote uses.Also fixed along the way. Picking a size with the mouse reopened the options menu, because the click bubbled through the portal back to the inline work package.
What works from the keyboard now
Checked manually with Orca, and each step is also covered by a browser test.
Selecting a work package
Options menu
Size submenu
Menu opened with the mouse
Merging together with BNE-169
BNE-169 (#26020) moves the outside press handlers of inline and block work packages into
usePressOutside, and this PR changes the same handlers. Whichever merges second keeps the hook and passes it thecloseOptionsfrom here.Sorry in advance for this much code in one PR. It is one feature end to end, and I promise the changes are worth it!
AI involvement
Collaborative – AI generated a substantial part of the code; I reviewed and understand every line.
Merge checklist