Content
View differences
Updated by Alexander Coles about 6 hours ago
### Goal
Give assistive technology consistent row counts and positions in the work package table: sibling positions, set sizes, accessible row indices and totals. This is the row-count part of the "Grid semantics" section of #DREAM-846, split from #DREAM-885 because it still depends on #DREAM-860's row-model decision.
### Starting point
#DREAM-885 exposes the table as a `treegrid` with levels, selection and expansion state. Browser-derived positions during the interval before this work lands are not evidence that the agreed row model is implemented; #DREAM-897 gates release. #DREAM-892's ledger supplies the rendered body-occurrence order and visibility. The complete accessible row model must also account for the column header, overall totals and create/empty-state rows outside that ledger.
#DREAM-860 owns the logical row model for each table mode: headers, group and sum rows, contextual ancestors, expanded relations, repeated work package occurrences, empty/create rows, collapsed rows and pagination. It must explicitly decide whether and how inline-create and empty-state rows participate in sibling positions and accessible row indices/totals. Related #DREAM-858 defines row indices for `OpPrimer::BorderBoxTableComponent`; coordinate conventions without assuming its count formula fits.
Navigability follows #DREAM-886; selection eligibility follows #DREAM-893; this work covers accessible counts and positions only. It consumes the roles and levels delivered by #DREAM-885 without changing them or deduplicating accessible row occurrences by selection membership. Semantics apply where `keyboardNavigationEnabled` is on; per-consumer activation follows #DREAM-896.
### Hierarchy ordering and staged delivery
Keep the existing query-page membership. For global hierarchy positions, use a depth-first traversal with sibling subtrees ordered by the earliest query rank of any matching work package in each subtree (including the parent when it matches). Include required contextual ancestors and render each page in that canonical order. For query order `[Child, Other, Parent]`, where Child belongs to Parent, the logical hierarchy order is `Parent, Child, Other`; the parent retains the same logical identity when encountered later as a primary result. Validate nested hierarchies and cross-page examples before finalizing the implementation.
Logical occurrence identities and coordinates must be independent of page size. Future infinite scrolling may merge or insert arriving rows into existing subtrees; pure append-only loading is not required. Infinite-scroll UI, canonical-order repagination and cross-request snapshot protocols are outside this implementation.
Frontend projection and missing backend metadata may be delivered in separate implementation chunks. A short, explicitly tracked compliance gap on `dev` is accepted between those chunks; this work package closes when https://github.com/opf/openproject/pull/26291 merges. remains incomplete until all supported modes meet the acceptance criteria. Partial delivery must not fabricate global positions. The first chunk, https://github.com/opf/openproject/pull/26291, delivers `aria-rowindex`/`aria-rowcount` for flat and grouped tables; hierarchy and expanded-relation positions, and positions after a reorder, are #DREAM-941 (API row-position metadata), expanded relation and children columns #DREAM-942 (after the #DREAM-944 decision), positions after a drop in grouped and unsaved manually sorted queries #DREAM-943, and metadata); sibling attributes a follow-up Implementation work package once #DREAM-860 decides wait for the sibling set. structural-row speech probe. Collapsed rows keep their global index and the table total keeps counting them (stable gaps, decided 2026-10-24, to be recorded in #DREAM-860); structural-row sibling participation still requires the recorded #DREAM-860 decision, and #DREAM-897 remains the release verification gate.
### Scope
1. Implement the #DREAM-860 row model for `aria-posinset`/`aria-setsize` among siblings and `aria-rowindex`/`aria-rowcount` for the accessible table, in flat, grouped and hierarchy modes. Distinguish the sibling set from the complete table row sequence.
2. Keep indices and totals consistent across pagination and with every exposed row category. Query-result positions are never presented as accessible positions where additional rows make them differ. A collection without a usable total withdraws every position; nothing is fabricated. Apply the agreed handling of omitted attributes consistently.
3. Update positions after group/hierarchy collapse, expansion, switching relation columns and re-renders. Preserve the existing work-package-wide relation expansion state across repeated occurrences; count each exposed occurrence according to the recorded model.
### Acceptance criteria
- A recorded #DREAM-860 row-model decision exists before implementation is finalized. Counts and positions match it in all three modes, covering headers, group/sum/totals rows, contextual ancestors, expanded relations, repeated occurrences, inline-create, empty results, collapsed rows and pagination.
- Focused assertions cover the agreed `aria-posinset`, `aria-setsize`, `aria-rowindex` and `aria-rowcount` values, including withdrawal on unusable totals and changes to exposed rows; #DREAM-885's existing axe checks stay green. Verify the whole accessible table, not only ledger rows.
- Consumers awaiting #DREAM-896 retain their current semantics. Existing suites stay green; frontend typecheck and touched-file lint pass. #DREAM-897 verifies the resulting positions and counts with screen readers before release.
### Out of scope
Roles, levels, selection and expansion state (#DREAM-885); the row model decision itself (#DREAM-860); focus and navigation (#DREAM-886); changing selection eligibility or membership (#DREAM-893); screen-reader verification (#DREAM-897).
### Hints for Reviewers
Using the recorded #DREAM-860 examples, compare the accessibility inspector's sibling positions, row indices and totals with the expected values. Cover pagination, collapsed groups/hierarchies, switching relation columns, repeated occurrences, empty results and inline-create. Default views use hierarchy mode, which the first chunk does not cover, so test with a flat or grouped query. Expanding a relation or children column clears every position until it is collapsed again. A drag clears them until the drop settles and a failed drop restores them; after a successful reorder they stay absent until the results reload. Verify actual position speech in #DREAM-897; do not assume every screen reader announces every exposed attribute.
Give assistive technology consistent row counts and positions in the work package table: sibling positions, set sizes, accessible row indices and totals. This is the row-count part of the "Grid semantics" section of #DREAM-846, split from #DREAM-885 because it still depends on #DREAM-860's row-model decision.
### Starting point
#DREAM-885 exposes the table as a `treegrid` with levels, selection and expansion state. Browser-derived positions during the interval before this work lands are not evidence that the agreed row model is implemented; #DREAM-897 gates release. #DREAM-892's ledger supplies the rendered body-occurrence order and visibility. The complete accessible row model must also account for the column header, overall totals and create/empty-state rows outside that ledger.
#DREAM-860 owns the logical row model for each table mode: headers, group and sum rows, contextual ancestors, expanded relations, repeated work package occurrences, empty/create rows, collapsed rows and pagination. It must explicitly decide whether and how inline-create and empty-state rows participate in sibling positions and accessible row indices/totals. Related #DREAM-858 defines row indices for `OpPrimer::BorderBoxTableComponent`; coordinate conventions without assuming its count formula fits.
Navigability follows #DREAM-886; selection eligibility follows #DREAM-893; this work covers accessible counts and positions only. It consumes the roles and levels delivered by #DREAM-885 without changing them or deduplicating accessible row occurrences by selection membership. Semantics apply where `keyboardNavigationEnabled` is on; per-consumer activation follows #DREAM-896.
### Hierarchy ordering and staged delivery
Keep the existing query-page membership. For global hierarchy positions, use a depth-first traversal with sibling subtrees ordered by the earliest query rank of any matching work package in each subtree (including the parent when it matches). Include required contextual ancestors and render each page in that canonical order. For query order `[Child, Other, Parent]`, where Child belongs to Parent, the logical hierarchy order is `Parent, Child, Other`; the parent retains the same logical identity when encountered later as a primary result. Validate nested hierarchies and cross-page examples before finalizing the implementation.
Logical occurrence identities and coordinates must be independent of page size. Future infinite scrolling may merge or insert arriving rows into existing subtrees; pure append-only loading is not required. Infinite-scroll UI, canonical-order repagination and cross-request snapshot protocols are outside this implementation.
Frontend projection and missing backend metadata may be delivered in separate implementation chunks. A short, explicitly tracked compliance gap on `dev` is accepted between those chunks; this work package closes when https://github.com/opf/openproject/pull/26291 merges.
### Scope
1. Implement the #DREAM-860 row model for `aria-posinset`/`aria-setsize` among siblings and `aria-rowindex`/`aria-rowcount` for the accessible table, in flat, grouped and hierarchy modes. Distinguish the sibling set from the complete table row sequence.
2. Keep indices and totals consistent across pagination and with every exposed row category. Query-result positions are never presented as accessible positions where additional rows make them differ. A collection without a usable total withdraws every position; nothing is fabricated. Apply the agreed handling of omitted attributes consistently.
3. Update positions after group/hierarchy collapse, expansion, switching relation columns and re-renders. Preserve the existing work-package-wide relation expansion state across repeated occurrences; count each exposed occurrence according to the recorded model.
### Acceptance criteria
- A recorded #DREAM-860 row-model decision exists before implementation is finalized. Counts and positions match it in all three modes, covering headers, group/sum/totals rows, contextual ancestors, expanded relations, repeated occurrences, inline-create, empty results, collapsed rows and pagination.
- Focused assertions cover the agreed `aria-posinset`, `aria-setsize`, `aria-rowindex` and `aria-rowcount` values, including withdrawal on unusable totals and changes to exposed rows; #DREAM-885's existing axe checks stay green. Verify the whole accessible table, not only ledger rows.
- Consumers awaiting #DREAM-896 retain their current semantics. Existing suites stay green; frontend typecheck and touched-file lint pass. #DREAM-897 verifies the resulting positions and counts with screen readers before release.
### Out of scope
Roles, levels, selection and expansion state (#DREAM-885); the row model decision itself (#DREAM-860); focus and navigation (#DREAM-886); changing selection eligibility or membership (#DREAM-893); screen-reader verification (#DREAM-897).
### Hints for Reviewers
Using the recorded #DREAM-860 examples, compare the accessibility inspector's sibling positions, row indices and totals with the expected values. Cover pagination, collapsed groups/hierarchies, switching relation columns, repeated occurrences, empty results and inline-create. Default views use hierarchy mode, which the first chunk does not cover, so test with a flat or grouped query. Expanding a relation or children column clears every position until it is collapsed again. A drag clears them until the drop settles and a failed drop restores them; after a successful reorder they stay absent until the results reload. Verify actual position speech in #DREAM-897; do not assume every screen reader announces every exposed attribute.