# Feature DREAM-867 - Port KeybindingHint component from Primer React

Project: Stream Design System
Status: in development
URL: https://community.openproject.org/work_packages/DREAM-867

## Planning

| Attribute | Value |
| --- | --- |
| Date |  |
| Priority | Medium |
| Target versions | 18.1.0 |
| Sprint |  |
| Backlog bucket |  |
| Votes |  |
| List | a11y report 2026 |
| Roadmap |  |
| Relevance |  |

## OPEN POINTS / FEEDBACK

_No work packages_

## Features and Chunks

| ID | Subject | Type | Status | Assignee | Target versions | Spent time |
| --- | --- | --- | --- | --- | --- | --- |
| DREAM-968 | Port KeybindingHint to Primer View Components with translatable key names | Implementation | closed |  | 18.0.0 |  |
| DREAM-969 | Show keybinding hints on Angular buttons and context menu items | Implementation | new |  |  |  |
| DREAM-971 | Add keybinding hint translations to OpenProject | Implementation | new |  |  |  |

## Bugs and other work packages

_No work packages_

## Technical Details

| Attribute | Value |
| --- | --- |
| Module | Modules and project features / Backlogs, Frontend components and Primer design system |
| Feature Flag |  |
| Requires doc change | No |
| Enterprise plan | Community (COM) |

## Details

| Attribute | Value |
| --- | --- |
| Pull preview |  |
| Mockups |  |
| Accessibility | Yes |

## People

| Attribute | Value |
| --- | --- |
| Assignee | Alexander Coles |
| Accountable |  |
| Product manager |  |
| Developers | Alexander Coles |
| Designer | Julia Racsko |
| Ordered by |  |
| Signed off by |  |
| Interest by |  |

## Estimates and progress

| Attribute | Value |
| --- | --- |
| PD (min) |  |
| PD (max) |  |
| Work |  |
| Labor costs |  |

## Description

### Goal

Port Primer React's [KeybindingHint](https://primer.style/product/components/keybinding-hint/) to `opf/primer_view_components` so buttons and [action menus](https://primer.style/product/components/action-menu/) can show visible, screen-reader-friendly keyboard shortcut hints, in Rails views and in Angular templates, in the user's language.

Reference implementation: `packages/react/src/KeybindingHint` in `primer/react`.

### Scope

Delivered in two Implementation chunks: the component in `opf/primer_view_components` (<mention class="mention" data-id="DREAM-968" data-type="work_package" data-text="#DREAM-968">#DREAM-968</mention>), then the Angular integration in OpenProject core (<mention class="mention" data-id="DREAM-969" data-type="work_package" data-text="#DREAM-969">#DREAM-969</mention>).

#### Primer View Components

  - `Primer::OpenProject::KeybindingHint` component: `keys:` in github/hotkey syntax (`+` chords, space-separated sequences, `Mod` pseudo-key), `format:` condensed/full, `scheme:` default/emphasis, `size:` medium/small.
  - `<keybinding-hint>` Catalyst element renders the chords client-side (only the browser knows the platform: ⌘ vs Ctrl). Server renders a plain-text `<kbd>` fallback. Light DOM so Button/ActionList state styles can reach the chord.
  - `data-platform` override (apple/windows/other) for previews and tests.
  - Translatable key names: visible labels (condensed and full) and accessible names, including "then" between sequence chords, follow the user's language: the Ruby component translates them with `I18n.t` and passes them to the element as data. Primer React's English names remain the fallback.
  - On non-Apple platforms, Control and `Mod` show the localised key word (e.g. `Ctrl`, `Strg`) instead of `⌃`, since that is what those keyboards print.
  - New `trailing_visual` slot type `keybinding_hint` on `Primer::Beta::Button` and `Primer::Alpha::ActionList::Item`, with the React CSS state rules ported (hover/active/disabled, per scheme).
  - Lookbook previews (incl. forced-platform snapshot), Ruby component tests, Cuprite system tests, changeset, static dump.

#### OpenProject (Angular)

  - An Angular component on the element's own tag (`selector: 'keybinding-hint'`, empty template) that sets its data attributes; template authors place it like a trailing visual.
  - Optional keybinding on `op-context-menu` items: the hint is hidden from assistive technology, the item keeps its label as its name and gets a translated shortcut description (`aria-describedby`), and `aria-keyshortcuts` is set for single chords only.
  - Passes the translated key names from `I18nService` to the element; the keys reach i18n-js through the export patterns in `config/i18n.yml`.

### Approaches considered

  - **Chosen: one implementation in Primer View Components, wrapped by an Angular component.** Rails views and Angular templates share one element, key tables and platform logic.
  - **Rejected: pure Angular port** in `frontend/src/app/shared/components/primer`. Quickest to ship and trivial to translate, but duplicates the key tables and platform logic, which would drift. Rails views would later need either an Angular custom element (empty until Angular boots, so layout shift) or a second implementation.
  - **Rejected: raw `<keybinding-hint>` in Angular templates.** Needs `CUSTOM_ELEMENTS_SCHEMA`, which <mention class="mention" data-id="DREAM-950" data-type="work_package" data-text="#DREAM-950">#DREAM-950</mention> avoids, and leaves nothing to own `aria-keyshortcuts` on the menu item.

### Wayfinding

**Destination:** Keybinding hints that buttons and menus can show in the user's language, in Rails views and Angular templates. They come from one `<keybinding-hint>` element in Primer View Components ([Port KeybindingHint to Primer View Components with translatable key names](https://community.openproject.org/wp/DREAM-968)), OpenProject's locale keys ([Add keybinding hint translations to OpenProject](https://community.openproject.org/wp/DREAM-971)) and an Angular component that wraps the element ([Show keybinding hints on Angular buttons and context menu items](https://community.openproject.org/wp/DREAM-969)). Consumers can then adopt them without further component work.

**Notes**

  - **State (2026-11-01, end of day):**
      - <mention class="mention" data-id="DREAM-968" data-type="work_package" data-text="#DREAM-968">#DREAM-968</mention> is implemented on <https://github.com/opf/primer_view_components/pull/534> (draft, needs human review). It has 9 commits: 3 for the component, 6 for translation, including `buildKeyNames` for Angular.
      - CI is green except "Visual", which fails on the Ubuntu 26.04 runner until <https://github.com/opf/primer_view_components/pull/546> (pins `ubuntu-24.04`) merges. <mention class="mention" data-id="546" data-type="work_package" data-text="#546">#546</mention> needs a reviewer.
      - <mention class="mention" data-id="DREAM-971" data-type="work_package" data-text="#DREAM-971">#DREAM-971</mention> and <mention class="mention" data-id="DREAM-969" data-type="work_package" data-text="#DREAM-969">#DREAM-969</mention> have not started. <mention class="mention" data-id="DREAM-971" data-type="work_package" data-text="#DREAM-971">#DREAM-971</mention> can start now: its locale keys don't depend on the PVC release.
  - **Design and plans** (repo `agent-plans`):
      - Design: `superpowers/specs/2026-11-01-keybinding-hint-translation-angular-design.md`. It holds the decisions below in detail.
      - Implemented plan: `superpowers/plans/2026-11-01-dream-968-keybinding-hint-translation.md`.
      - Reviews: `reviews/2026-11-01-keybinding-hint-design-review-consolidated.md`.
      - Handoff: `handoffs/2026-11-01-dream-867-keybinding-hint-status.md`.
  - **PVC working rules:**
      - Keep the CI "Generating component snapshots" commits unless later changes affect visuals; never regenerate snapshots locally.
      - Regenerate `static/` in each commit (`script/build-assets css && bundle exec rake static:dump`).
      - Run only the KeybindingHint system tests locally, after refreshing the stale demo builds (`script/build-assets js`, delete `demo/public/vite-test`, `npm run build:css` in `demo/`).
  - Rendering rule for PVC custom elements (the host renders and the element adds behaviour; `<keybinding-hint>` is the exception, because only the browser knows the platform): KB draft ADR `drafts/pvc-custom-elements-rendering.md`, to graduate to `docs/decisions`.
  - Reference implementation: `packages/react/src/KeybindingHint` in `primer/react`. `key_names.ts` and `platform.ts` mirror it verbatim.

**Decisions so far**

  - **Approach:** one implementation in Primer View Components, wrapped for Angular. A pure Angular port and raw `<keybinding-hint>` in Angular templates were rejected (see Approaches considered).
  - **Angular wrapper:** a component on the `keybinding-hint` tag with an empty template, not a directive that appends into its host.
  - **Translation:**
      - Visible key names (condensed and full), accessible names and "then" follow the user's language.
      - Off Apple, Control and `Mod` show `Ctrl` (German `Strg`) instead of `⌃`.
      - Mac symbols (`⌘`, `⌥`, `⌃`) and other glyphs aren't translated.
  - **Translation mechanism for 18.0, the `data-names` path:**
      - The Ruby component looks up the names for its keys in the `keybinding_hint` locale scope and passes them to the element as JSON in `data-names`. The server-rendered fallback uses them too.
      - The element looks up each name by exact platform, then `other`, then Primer React's English tables.
      - Translator data is untrusted: malformed or blank values are ignored, and symbols can't be replaced.
      - Angular builds the same JSON with PVC's `buildKeyNames`. A system parity test keeps it equal to Ruby.
      - Switching to the host's `window.I18n` is deferred to [Choose the long-term translation mechanism for PVC JavaScript strings](https://community.openproject.org/wp/DREAM-970). A Codex adversarial review set its preconditions, which are listed there.
  - **OpenProject locale keys:** in [Add keybinding hint translations to OpenProject](https://community.openproject.org/wp/DREAM-971), together with the i18n-js export and the PVC bump, so Rails hints (e.g. <mention class="mention" data-id="AGILE-380" data-type="work_package" data-text="#AGILE-380">#AGILE-380</mention>) are translatable in 18.0 even if the Angular work is cut. Crowdin gets no symbols and no `key:` fields. <mention class="mention" data-id="DREAM-969" data-type="work_package" data-text="#DREAM-969">#DREAM-969</mention> follows <mention class="mention" data-id="DREAM-971" data-type="work_package" data-text="#DREAM-971">#DREAM-971</mention>.
  - **Menus:**
      - The hint inside a menu item is hidden from assistive technology.
      - The item gets a translated shortcut description through `aria-describedby`.
      - `aria-keyshortcuts` is set only for single chords.
      - A screen-reader check is required before <mention class="mention" data-id="DREAM-969" data-type="work_package" data-text="#DREAM-969">#DREAM-969</mention> merges.
  - **18.0 cut rule:**
      - If the PVC release isn't published by 2026-11-06, the context-menu part of <mention class="mention" data-id="DREAM-969" data-type="work_package" data-text="#DREAM-969">#DREAM-969</mention> moves to 18.1. If it isn't published by 2026-11-09, all of <mention class="mention" data-id="DREAM-969" data-type="work_package" data-text="#DREAM-969">#DREAM-969</mention> moves.
      - OpenProject merges only against released PVC packages.
      - The cut rule doesn't apply to <mention class="mention" data-id="DREAM-971" data-type="work_package" data-text="#DREAM-971">#DREAM-971</mention>.
  - **Terminology:** `scheme` in both PVC and Angular; `variant` stays Primer React's term.
  - **`@primer/primitives` peer range:** raised to `^11.5.1`, the first release with the `--buttonKeybindingHint-*` tokens.
  - **Rails consumers:** the Backlogs card menu (<mention class="mention" data-id="AGILE-380" data-type="work_package" data-text="#AGILE-380">#AGILE-380</mention>) uses the PVC component directly.

**Open questions**

  - None at present.

**Not yet specified**

  - Rebasing <mention class="mention" data-id="DREAM-968" data-type="work_package" data-text="#DREAM-968">#DREAM-968</mention> onto the upstream sync in <https://github.com/opf/primer_view_components/pull/545> if that merges first (`static/` regenerates; snapshots regenerate if visuals change).
  - Known test gaps deferred from the final review of \#DREAM-968:
      - no test pins the English values in `en.yml` to upstream's output;
      - no German rendering test for `Plus`/`Space`;
      - no test where a translated exact-platform name beats `other`.
  - An upstream Primer React issue for `Mod` sorting before other modifiers.

**Out of scope for this map**

  - The long-term translation mechanism for PVC JavaScript (18.1): [Choose the long-term translation mechanism for PVC JavaScript strings](https://community.openproject.org/wp/DREAM-970). It also receives the context-menu part of <mention class="mention" data-id="DREAM-969" data-type="work_package" data-text="#DREAM-969">#DREAM-969</mention> if the cut rule triggers.
  - Adding hints to specific menus and buttons: <mention class="mention" data-id="AGILE-380" data-type="work_package" data-text="#AGILE-380">#AGILE-380</mention> (Backlogs card menu), Boards, <mention class="mention" data-id="DREAM-846" data-type="work_package" data-text="#DREAM-846">#DREAM-846</mention> and <mention class="mention" data-id="DREAM-876" data-type="work_package" data-text="#DREAM-876">#DREAM-876</mention> (work package table).
  - Tooltip keybinding hint container and IconButton `keybinding_hint:` param.
  - Binding shortcuts; the component only displays hints.

### Non-goals (follow-ups)

  - Tooltip keybinding hint container and IconButton `keybinding_hint:` param.
  - Consumers: adding hints to specific menus and buttons (Backlogs card menu <mention class="mention" data-id="AGILE-380" data-type="work_package" data-text="#AGILE-380">#AGILE-380</mention>, Boards, work package table <mention class="mention" data-id="DREAM-846" data-type="work_package" data-text="#DREAM-846">#DREAM-846</mention> / <mention class="mention" data-id="DREAM-876" data-type="work_package" data-text="#DREAM-876">#DREAM-876</mention>).
  - Binding shortcuts. The component displays hints only.

### Acceptance criteria

  - `render(Primer::OpenProject::KeybindingHint.new(keys: "Mod+Shift+p"))` shows ⌘⇧P on macOS and Ctrl⇧P elsewhere, with accessible text "command shift p" in English.
  - With German translations loaded, the same hint shows `Strg⇧P` outside macOS, and accessible names and "then" are German.
  - `with_trailing_visual_keybinding_hint(keys: ...)` works on Button (all schemes and sizes) and ActionList items.
  - In OpenProject, an Angular button or `op-context-menu` item can show a keybinding hint; menu items announce the translated shortcut as their description and expose single chords through `aria-keyshortcuts`.
  - Lookbook previews and system tests cover formats, sizes, schemes, sequences, all three platforms and a non-English locale.
