Skip to content

[COMMS-892] Improve accessibility (keyboard navigation & screen reader support) for WPs - #26027

Open
ihordubas99 wants to merge 2 commits into
devfrom
feature/comms-892-improve-accessibility-keyboard-navigation-screen-reader-support-for-wps
Open

ihordubas99 wants to merge 2 commits into
devfrom
feature/comms-892-improve-accessibility-keyboard-navigation-screen-reader-support-for-wps

Conversation

@ihordubas99

Copy link
Copy Markdown
Collaborator

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, as PasteWorkPackageLinkExtension already 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-state is now external and a peer dependency. It used to be bundled, so the package had its own copy and instanceof NodeSelection was 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

  • Enter or Space on the selected work package opens the menu, with focus on "Open".
  • ←/→ move between the items, Home and End jump to the first and last.
  • Enter on "Open" opens the work package in a new tab.
  • Enter on "Remove" removes it, and focus returns to the editor.
  • Escape or Tab close the menu and return focus to the editor. The work package stays selected, so Enter reopens the menu.

Size submenu

  • Enter on the size item opens the submenu, with focus on the current size.
  • ↑/↓ move between the sizes, and Enter picks one. The menu closes and focus returns to the editor.
  • Escape closes the submenu.

Menu opened with the mouse

  • Focus stays in the editor, as before.
  • Escape or Tab now only close the menu, instead of blurring the editor or nesting the block.

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 the closeOptions from 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

  • Added/updated tests
  • Added/updated documentation in Lookbook (patterns, previews, etc)
  • Tested major browsers (Chrome, Firefox, Edge, ...)

@ihordubas99 ihordubas99 self-assigned this Oct 14, 2026
@github-actions github-actions Bot added the ai: Collaborative 💻 AI generated a substantial part of the code; A human reviewed and understands every line. label Oct 14, 2026
@ihordubas99 ihordubas99 changed the title [COMMS-892] Improve accessibility (keyboard navigation & screen reade… [COMMS-892] Improve accessibility (keyboard navigation & screen reader support) for WPs Oct 20, 2026
@ihordubas99
ihordubas99 marked this pull request as ready for review October 20, 2026 08:44

@judithroth judithroth left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  1. 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
  2. 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
  1. 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?

@ihordubas99
ihordubas99 force-pushed the feature/comms-892-improve-accessibility-keyboard-navigation-screen-reader-support-for-wps branch from 6bb42ee to 0076a97 Compare October 20, 2026 12:04
@ihordubas99
ihordubas99 marked this pull request as draft October 20, 2026 12:04
@github-actions

Copy link
Copy Markdown

Caution

The Enterprise plan field is not set on the work package

Details:

Please make sure that:

  • The work package Enterprise plan field is set

@github-actions

Copy link
Copy Markdown

Warning

Flaky specs

  • rspec ./spec/features/work_packages/cancel_editing_spec.rb[1:5]
🤖 Ask Copilot to investigate

Copy 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.

@copilot The following spec(s) are flaky in CI (first seen on PR #26027, linked for reference only):

- `rspec ./spec/features/work_packages/cancel_editing_spec.rb[1:5]`

Treat this as a standalone task, unrelated to PR #26027. Create a new branch from origin/dev and open a new pull request targeting dev — do not stack it on PR #26027 or reuse that branch.

Follow the playbook in docs/development/testing/handling-flaky-tests/README.md to find the root cause and fix the underlying race — do not skip, delete, or weaken the spec to make it pass; disabling is a last resort per the playbook, and only with a bug ticket. Verify the fix by running the spec(s) repeatedly (e.g. `script/bulk_run_rspec --run-count 10`).

If you cannot reproduce the flake or are not confident in a fix after reasonable investigation, do not fabricate a change or skip the spec to force CI green. Instead, leave the pull request in draft and document what you tried, the suspected cause, and any leads in its description, then assign @ihordubas99 to take over.

Once the fix is verified, title the PR after the spec(s) it fixes, and use the PR description to explain the root cause, how the change resolves it, and the before/after results. Label the PR `flaky-spec`, assign @ihordubas99, and request a review from @ihordubas99.
On every commit, set @ihordubas99 as the sole co-author with a `Co-authored-by:` trailer (use their GitHub no-reply email so it links to their account), so it is traceable who dispatched the fix.

@ihordubas99

Copy link
Copy Markdown
Collaborator Author

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 .angular folder in frontend? Maybe something got cached there.

Screencast.from.2026-10-20.16-33-42.mp4

About the third point, eslint-plugin-jsx-a11y fits best. The frontend already has it as a dependency but never enables it, and the Angular templates are already covered by angular-eslint's templateAccessibility rules. I'd enable it in a follow-up PR for op-blocknote-extensions and the React code in the frontend, with a few exceptions for things the editor does on purpose (e.g. keeping work packages out of the tab order). It will flag existing code outside this feature, so a separate PR seems cleaner than growing this one.

While testing, I also found a bug with the focus ring inside the options menu itself, and fixed it in the last commit.

@ihordubas99
ihordubas99 marked this pull request as ready for review October 20, 2026 13:37
@ihordubas99
ihordubas99 force-pushed the feature/comms-892-improve-accessibility-keyboard-navigation-screen-reader-support-for-wps branch 2 times, most recently from 3a75e74 to e43cb62 Compare October 26, 2026 11:43

@judithroth judithroth left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks a lot for rebasing this 💙

Some findings:

  1. 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

  2. My screenreader (the default of Ubuntu) also reads the status for the "compact" size, which does not show the status on the page.

  3. (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.',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍


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 () => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 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';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

@ihordubas99
ihordubas99 force-pushed the feature/comms-892-improve-accessibility-keyboard-navigation-screen-reader-support-for-wps branch from e43cb62 to 0c90fd5 Compare October 26, 2026 14:51

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai: Collaborative 💻 AI generated a substantial part of the code; A human reviewed and understands every line.

Development

Successfully merging this pull request may close these issues.

2 participants