Skip to content
Merged
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
Original file line number Diff line number Diff line change
@@ -1,27 +1,38 @@
# Tasks — keyboard-accessible-widget-repositioning

> Archive pass 2026-10-07: every task ticked from code. `src/components/DashboardGrid.vue` was deleted when the grid moved to nextcloud-vue's `CnDashboardGrid`, so tasks 1 to 3 landed there (nextcloud-vue `development`):
> - Task 1: `src/components/CnDashboardGrid/CnDashboardGrid.vue:35-38` (`role="group"`, `:aria-label`, `:tabindex`).
> - Task 2: `CnDashboardGrid.vue:1055` (`.grid-stack-item:focus-visible`).
> - Task 3: `CnDashboardGrid.vue:633` (Enter and Space) and `:845-867` (`item-activate`, then a synthesised `contextmenu` that launchpad's `@contextmenu` handler at `src/views/Views.vue:290` opens the menu from).
> - Task 4: `src/components/Widgets/WidgetContextMenu.vue:26` (Move) and `:110` (`move` emit).
> - Task 5: `src/modals/WidgetMovePanel.vue` (in `src/modals/`, per the modal-isolation rule).
> - Task 6: `src/composables/useGridManager.js:248` (`nudgePlacement`).
> - Task 7: `src/views/Views.vue:420` and `:444-450`.
> - Task 8: `src/modals/__tests__/WidgetMovePanel.spec.js`. Task 9: `src/components/Widgets/__tests__/WidgetContextMenu.spec.js:59`.
> - Tasks 10 and 11: `tests/e2e/keyboard-widget-repositioning.spec.ts` (keyboard-only move, focus trap), plus nextcloud-vue `tests/a11y/CnDashboardGrid.a11y.spec.js` and `tests/components/CnDashboardGridKeyboard.spec.js`.

## Grid item accessibility baseline

- [ ] Task 1: In `src/components/DashboardGrid.vue`, add `role="group"`,
- [x] Task 1: In `src/components/DashboardGrid.vue`, add `role="group"`,
`:aria-label="getPlacementAriaLabel(placement)"` (new computed helper
returning e.g. `Widget: {title}` or `Tile: {title}`), and `tabindex="0"`
to the `.grid-stack-item` element (currently lines 9-20).
- [ ] Task 2: Add `:focus-visible` CSS to `.grid-stack-item` using NC
- [x] Task 2: Add `:focus-visible` CSS to `.grid-stack-item` using NC
variables (`var(--color-primary-element)` outline), matching the
existing visual language of the component's other interactive states.
- [ ] Task 3: Wire `keydown.enter` and `keydown.space` on the grid item (in
- [x] Task 3: Wire `keydown.enter` and `keydown.space` on the grid item (in
edit mode only) to open the same context-menu / move-panel flow that
right-click already triggers, calling the existing `onItemContextMenu`
handler with a synthesized position (e.g. centered on the focused
element's bounding rect) instead of `event.clientX/clientY`.

## Move action

- [ ] Task 4: In `src/components/Widgets/WidgetContextMenu.vue`, add a new
- [x] Task 4: In `src/components/Widgets/WidgetContextMenu.vue`, add a new
`role="menuitem"` button "Move" between the existing Edit (line 13) and
Remove (line 29) buttons, with a new `onMove()` handler emitting a new
`move` event (add `'move'` to the `emits` array at line 88) then `close`.
- [ ] Task 5: Create `src/components/Widgets/WidgetMovePanel.vue` — a
- [x] Task 5: Create `src/components/Widgets/WidgetMovePanel.vue` — a
focus-trapped panel/modal (reuse `NcModal` or a lightweight popover
matching `WidgetContextMenu`'s positioning approach) that:
- Displays the widget's current `gridX/gridY/gridWidth/gridHeight`.
Expand All @@ -33,29 +44,29 @@
`Escape` to cancel (emit `close` with no change).
- Shows a live text readout of the pending position/size for screen
readers (`aria-live="polite"`).
- [ ] Task 6: In `src/composables/useGridManager.js`, extract a pure
- [x] Task 6: In `src/composables/useGridManager.js`, extract a pure
helper (e.g. `nudgePlacement(placement, direction, allPlacements)`) that
computes the candidate new rect and reuses the same collision/pushing
logic `placeNewWidget()` (lines 232+) already implements, so keyboard
moves get the same collision-avoidance behaviour as drag moves.
- [ ] Task 7: Wire `DashboardGrid.vue`'s `move` event (from the context
- [x] Task 7: Wire `DashboardGrid.vue`'s `move` event (from the context
menu) to open `WidgetMovePanel`, and on `save` call the existing
placement-update path (the same one GridStack's `change` event already
uses) with the panel's resulting rect.

## Tests

- [ ] Task 8: Add a Vitest test for `WidgetMovePanel.vue` asserting arrow
- [x] Task 8: Add a Vitest test for `WidgetMovePanel.vue` asserting arrow
keys nudge the displayed position, `Shift+Arrow` resizes, `Enter` emits
`save` with the expected rect, and `Escape` emits `close` with no `save`.
- [ ] Task 9: Add a Vitest test for `WidgetContextMenu.vue` asserting the
- [x] Task 9: Add a Vitest test for `WidgetContextMenu.vue` asserting the
new Move button emits `move` then `close`.
- [ ] Task 10: Add a Playwright e2e test (per `feedback_playwright-ui-only-newman-api.md`
- [x] Task 10: Add a Playwright e2e test (per `feedback_playwright-ui-only-newman-api.md`
— UI-driven, not API-direct) that: focuses a grid item via `Tab`, opens
the move panel via `Enter`, nudges it right twice via arrow keys, confirms
via `Enter`, and asserts the widget's rendered grid position changed by
the expected offset without any pointer/mouse events being dispatched.
- [ ] Task 11: Add an axe-core (or equivalent) accessibility check to the
- [x] Task 11: Add an axe-core (or equivalent) accessibility check to the
existing Playwright/Vitest suite asserting each grid item has a
discoverable accessible name and that the move panel traps focus
correctly (no focus leak to the canvas behind it while open).
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ provisions one instance. Faking the registry inside the suite would test the
fake. `StoreServiceTest` covers the install path against the engine's real
signatures.

## NEW Requirements
## ADDED Requirements

### Requirement: REQ-STORE-001 The store routes MUST exist wherever the store page is declared

Expand Down
2 changes: 1 addition & 1 deletion openspec/parity/capabilities.json
Original file line number Diff line number Diff line change
Expand Up @@ -606,7 +606,7 @@
"state": "built",
"evidence": "nextcloud-vue CnDashboardGrid.vue:35-47 (main 0f1c0ffbb) makes every grid item in edit mode a focusable named group (role group, aria-label, tabindex 0 while keyboardRepositioning, default true) and turns Enter or Space into the contextmenu event a right-click sends; src/views/Views.vue:290 catches it on the placement and opens WidgetContextMenu; src/components/Widgets/WidgetContextMenu.vue:22-27 offers Move, which src/views/Views.vue:420 and :444 route to src/modals/WidgetMovePanel.vue; WidgetMovePanel.vue:286-310 moves with the arrow keys, resizes with Shift and the arrow keys, saves on Enter and cancels on Escape, through nudgePlacement (src/composables/useGridManager.js:248), the collision model the drag path uses; Views.vue:1964 handleMoveSave persists it. Tests: src/modals/__tests__/WidgetMovePanel.spec.js, tests/e2e/keyboard-widget-repositioning.spec.ts (drives the page with the keyboard only, checks the position after reload)",
"owner": "ConductionNL/launchpad",
"change": "openspec/changes/keyboard-accessible-widget-repositioning",
"change": "openspec/changes/archive/2026-10-07-keyboard-accessible-widget-repositioning",
"readOn": "2026-10-07"
},
"reachedOn": "Workspace page, widget context menu, Move",
Expand Down
47 changes: 47 additions & 0 deletions openspec/specs/beta-alignment/spec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# beta-alignment Specification

## Purpose
LaunchPad is described in four places: the app metadata in `appinfo/info.xml`, the pages in `src/manifest.json`, the product page on conduction.nl and the documentation. This capability keeps those four in step: they use the same feature vocabulary, the licence tag matches the licence text, and a translated product page lives at the same relative path as the page it translates.

## Requirements

### Requirement: The four public surfaces SHALL agree on feature vocabulary
`appinfo/info.xml`, the shipped feature set in `src/manifest.json` / `lib/`,
the product page (`conduction-website/src/pages/apps/launchpad.mdx` + its
Dutch translation), and `docs/` SHALL describe the same canonical feature
list using the same feature names, and SHALL NOT assert a capability that
is not present in `lib/` or `src/` at HEAD.

#### Scenario: A feature bullet on the product page names a real capability
- **WHEN** the product page or docs describe a LaunchPad capability
- **THEN** that capability SHALL be traceable to a concrete file in `lib/`
or `src/` (a controller, service, widget renderer, or registry entry)

#### Scenario: info.xml does not declare an install-time dependency the architecture forbids
- **WHEN** `docs/architecture.md` documents a "MUST NOT" install-time
dependency rule for an app (e.g. OpenRegister)
- **THEN** `appinfo/info.xml`'s `<dependencies>` block SHALL NOT list that
app

### Requirement: The license tag SHALL match the declared license text
`appinfo/info.xml`'s `<licence>` element and its top-of-file SPDX header
SHALL match the license asserted in both `<description>` CDATA blocks
("Free and open source under the EUPL-1.2 license") and every PHP file's
`@license` docblock tag.

#### Scenario: licence tag matches description text
- **WHEN** `info.xml`'s description states a license
- **THEN** the `<licence>` element SHALL declare the same license

### Requirement: A translated product page SHALL live at the same relative path as its source page
Docusaurus i18n resolves a translated page by matching the source page's
file path under the locale's content directory. A translation SHALL NOT be
left at a stale pre-rename filename after the English source page is
renamed.

#### Scenario: English product page is renamed
- **WHEN** `conduction-website/src/pages/apps/<old-slug>.mdx` is renamed to
`<new-slug>.mdx`
- **THEN** every locale's translation under
`i18n/<locale>/docusaurus-plugin-content-pages/apps/` SHALL be renamed to
the same `<new-slug>.mdx`, and no stale `<old-slug>.mdx` file SHALL remain
Loading
Loading