Skip to content

[DREAM-894] Keyboard selection, context actions and selection feedback in the work package table - #26294

Open
myabc wants to merge 8 commits into
feature/dream-938-row-positionsfrom
feature/dream-894-keyboard-selection
Open

myabc wants to merge 8 commits into
feature/dream-938-row-positionsfrom
feature/dream-894-keyboard-selection

Conversation

@myabc

@myabc myabc commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

Note

This PR is stacked on #26291 (DREAM-938, global row positions). Please review/merge that PR first.

Ticket

https://community.openproject.org/wp/DREAM-894

What are you trying to accomplish?

tl;dr for humans: Keyboard users can now select work packages and open their context menu without a mouse, and screen readers announce how many are selected.

  • Shift+Space toggles the focused work package and anchors it; Shift+Arrow Up/Down extend the range like Shift-click. Ctrl/Cmd+A selects every work package on the page.
  • Shift+F10 and the Menu key open the row's context menu once (Shift+Alt+F10 still works). Escape or an action returns focus to the row or cell, even after a re-render.
  • Selection changes are announced politely ("3 work packages selected.", "Selection cleared."), for pointer and keyboard alike.

What approach did you choose and why?

The table's keyboard owner handles selection chords before navigation, only on the focused roving stop, so editors and inner controls keep their keys. Selection still goes through WorkPackageViewSelectionService, which gains a no-anchor Select All.

Announcements use a small engine extracted from the Backlogs sortable-lists orchestrator into core-common. Only gesture sites on the table call it, so card view, boards, the calendar and programmatic changes stay silent.

Context-menu chords stay in ContextMenuKeyboardHandler; the browser's companion contextmenu event is matched to the keyboard-opened row, and any pointerdown cancels the match. Focus returns only on Escape or an action, never on scroll or click-away.

Watch areas

  • isSelectAllShortcut now rejects Shift for every caller (table, card view, Backlogs), implementing OP-20202.
  • Outside the table: the orchestrator migration (first commit), the Shift rejection (second), and OPContextMenuService.close passing a close reason to every menu's onClose (seventh).
  • On keyboard-enabled tables, clicks are announced too, and right-click in an open inline editor shows the browser menu. Other tables keep today's behaviour.

Out of scope: speech verification (DREAM-897), embedded tables (DREAM-896), APG-strict Escape (DREAM-940).

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, ...)
  • Manual QA: Shift+F10 and Menu key on Windows Chrome/Edge (companion contextmenu on keyup; release the key only after the menu has taken focus), macOS Safari/Firefox, and VoiceOver VO+Shift+M — menu opens once, on the right target set, focus returns on Escape
  • Manual QA: announcements with a screen reader for same-count range changes and page-scoped Select All

@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 24, 2026
@myabc myabc added feature javascript Pull requests that update Javascript code Accessibility labels Oct 24, 2026
@myabc myabc added this to the 18.0.x milestone Oct 24, 2026
@myabc
myabc marked this pull request as ready for review October 24, 2026 21:58
@github-actions

github-actions Bot commented Oct 24, 2026 •

Copy link
Copy Markdown

Deploying openproject with ⚡ PullPreview

Field Value
Latest commit ae9a7ef
Job deploy
Status ✅ Deploy successful
Preview URL https://pr-26294-dream-894-keyboar-ip-49-12-209-227.my.opf.run:443

View logs

@myabc
myabc force-pushed the feature/dream-894-keyboard-selection branch 2 times, most recently from e021aef to c78bfec Compare October 24, 2026 22:57
@myabc
myabc force-pushed the feature/dream-894-keyboard-selection branch 2 times, most recently from 811be99 to e64b6c0 Compare October 26, 2026 02:35
@myabc
myabc force-pushed the feature/dream-938-row-positions branch from 6ec3530 to ed11e52 Compare October 26, 2026 03:52
@myabc
myabc force-pushed the feature/dream-894-keyboard-selection branch from e64b6c0 to ee4c332 Compare October 26, 2026 03:52
@myabc myabc changed the title [DREAM-894] Add keyboard selection and context menus to the table [DREAM-894] Keyboard selection, context actions and selection feedback in the work package table Oct 26, 2026
@github-actions

Copy link
Copy Markdown

Warning

Flaky specs

  • rspec ./modules/my_page/spec/features/my/my_spent_time_widget_with_a_negative_time_zone_spec.rb[1:1]
🤖 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 #26294, linked for reference only):

- `rspec ./modules/my_page/spec/features/my/my_spent_time_widget_with_a_negative_time_zone_spec.rb[1:1]`

Treat this as a standalone task, unrelated to PR #26294. Create a new branch from origin/dev and open a new pull request targeting dev — do not stack it on PR #26294 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 @myabc 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 @myabc, and request a review from @myabc.
On every commit, set @myabc 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.

@myabc
myabc force-pushed the feature/dream-894-keyboard-selection branch from ee4c332 to 7b5358d Compare October 26, 2026 11:13
myabc added 8 commits October 26, 2026 14:32
Extracts the orchestrator's announcement policy into a shared
SelectionAnnouncements class in core-common, so other selection
consumers can reuse the gesture rules and the render baseline.

Moves the navigation/selection speak rules and the baseline tracking
out of renderSelection. The orchestrator now delegates to the engine
and drops its own selectionMessage helper. Its spec is unchanged.

https://community.openproject.org/wp/DREAM-894
Ctrl/Cmd+Shift+A is not Select All. The shared predicate
isSelectAllShortcut now returns false when Shift is held, so the
shortcut falls through to the browser instead of selecting every
row (OP-20202).

This applies to every caller of the shared predicate: the work
package table, the card view, and Backlogs.

https://community.openproject.org/wp/DREAM-894
Adds a null anchor mode to selectAll that selects every selectable
occurrence and clears any existing anchor, so a following Shift
gesture falls back to a plain replace. Omitting the argument keeps
the first-selectable fallback. Adds a selectionKeys getter for
callers that compare membership across a gesture.

https://community.openproject.org/wp/DREAM-894
Adds TableSelectionFeedback, created by the selection transformer
only for tables with keyboard navigation enabled. Gesture sites (row
click, context menu, drag collapse and Escape) run their mutation
through selectionGesture, which announces via the shared
SelectionAnnouncements engine. Capability-off tables run the mutation
unchanged.

The feedback records the membership each gesture commits as owned
by the table; any other publication with a different membership
drops that ownership. Pruning after a render announces only while
the membership is still gesture-owned, so route, reload and
programmatic selections are pruned silently.

Adds the js.work_packages.selection locale keys. The card view is
covered by a zero-announce test in its click handler spec; the shared
selection services stay unhooked.

https://community.openproject.org/wp/DREAM-894
Adds the Shift+Space, Shift+ArrowUp/Down and Ctrl/Cmd+A chords to the
capability-on work package table. A new KeyboardSelection collaborator
resolves the eligible rendered occurrence at the roving position and
runs toggle, range and select-all through the announcement feedback.

KeyboardNavigationTransformer dispatches to it on the roving stop
before navigation. Shift+Arrow moves focus first and extends the range
only when focus landed on a selectable row, so group headers and sums
rows leave the range unchanged. Select All anchors the occurrence under
focus, or none from a structural row.

The legacy capture-phase select-all registration is now skipped for
capability-on tables, so Ctrl/Cmd+A is handled exactly once.

https://community.openproject.org/wp/DREAM-894
Opens the work package row context menu from the keyboard with
Shift+F10 and the Menu (ContextMenu) key, in addition to the existing
Shift+Alt+F10. Cell focus resolves to its row, and group headers, the
column header and totals stay inert. Repeat keydowns of a held chord
are swallowed, so holding the key opens the menu once.

Browsers fire a companion contextmenu event after these keys. The
shared ContextMenuHandler records each keyboard open per table with
its row and swallows the first contextmenu on that row within 500 ms,
so a keyboard invocation opens the menu exactly once. A repeat
keydown on the same row refreshes the record, so the companion after
a long hold is still swallowed. A pointerdown on the table or the
table's teardown forgets the record, and a right-click on another
row, or after the window, opens normally.

Inside an editable control (input, textarea, select, contenteditable
other than contenteditable="false", or an active edit field) both the
chords and right-click are left to the browser, so the native menu
shows instead of the row menu. Both handlers share one predicate for
this. The work package id link keeps its native link menu for the
chords as it already does for right-click.

The new chords, the editor refusal and the id link exemption apply
only to tables with keyboard navigation. Other tables keep their
previous behaviour: only Shift+Alt+F10 opens the row menu, also on
the id link and inside an editor, and right-click inside an editor
opens the row menu.

https://community.openproject.org/wp/DREAM-894
Focus in the body-portal context menu marks focus as having left the
table, so neither recovery nor returnFocus restored it after a menu
was dismissed with Escape or closed by an action.

OPContextMenuService.close now carries a reason (escape, action,
outside or scroll) through to the handler's onClose. The row and
column context menus capture their origin position at open and
return focus through it only for Escape and action closes; a click
outside or a scroll closes the menu as before without moving focus.
Tables without keyboard navigation keep the previous behaviour.

Adds returnFocusFromMenu to KeyboardNavigation: it runs the
returnFocus logic with its userPosition guard while ignoring the
left-table flag, and clears the flag and claims ownership for a
pending render only when focus was actually returned. It never takes
focus from a control outside the table and is a no-op once the table
is destroyed.

Lets Escape cancel a context menu that is still opening, so the
keypress closes it instead of being refused; a click outside during
the opening stays refused.

The context menu overlay prevents the browser's own menu for any
contextmenu inside it, so a keyboard companion contextmenu that lands
on the focused menu item does not show the native menu over ours.

Closing a row menu for any reason forgets the table's keyboard-open
record, so a contextmenu on the same row right after the close opens
the menu again instead of being swallowed as a companion. The record
is noted after the menu is shown, so closing a menu it replaces does
not forget it.

https://community.openproject.org/wp/DREAM-894
Adds Components::LiveRegion, a Capybara support component that reads
Primer's polite live region through its shadow root and offers a
synchronized announcement expectation.

Adds a feature spec for the work package table covering Shift+Space
toggling, Shift+Arrow ranges, Select All, Escape, Shift+F10 context
menus with focus return, mixed pointer and keyboard gestures and the
selection announcements, in flat, grouped and hierarchy tables.

https://community.openproject.org/wp/DREAM-894
@myabc
myabc force-pushed the feature/dream-894-keyboard-selection branch from 7b5358d to ae9a7ef Compare October 26, 2026 14:32
@myabc
myabc force-pushed the feature/dream-938-row-positions branch from ed11e52 to 866679a Compare October 26, 2026 14:32

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

Accessibility ai: Collaborative 💻 AI generated a substantial part of the code; A human reviewed and understands every line. feature javascript Pull requests that update Javascript code needs review pullpreview

Development

Successfully merging this pull request may close these issues.

1 participant