Repository navigation
[QTI] Choice editor clickable regions are mouse-only and lack accessible semantics #6043
Description
❌ This issue is not open for contribution. Visit Contributing guidelines to learn about the contributing process and how to find suitable issues.
Target branch: unstable
Observed behavior
The QTI Choice editor makes a whole region clickable to enter edit mode by attaching a bare @click handler to a plain <div>. This happens in two places in ChoiceInteractionEditor.vue:
- the prompt wrapper (
@click="handlePromptClick") - each choice card,
.choice-border(@click="handleChoiceClick")
Neither container carries any interactive semantics — no tabindex, no role="button", no aria-label. The result is that entering edit mode is a mouse-only action: a keyboard user has no way to open the editor for the prompt or for a choice. Assistive technology gets no announcement that the region is actionable or what activating it would do.
The containers can't simply be turned into <button> elements, because they render other interactive content inside them — the TipTapEditor and its toolbar, the KRadioButton/KCheckbox selection control, and the per-choice action buttons. Nesting those inside a button is invalid HTML and breaks their own interaction.
Expected behavior
Entering edit mode is reachable by keyboard and announced to assistive technology, without breaking the interactive content that lives inside the region.
A shared ClickableRegion component provides this. It renders an invisible button that fills the container and sits behind the region's content:
<div class="container">
<button class="invisible-hoverable-button" aria-label="…" />
<!-- content renders as a sibling of the button, not a child -->
</div>Because the content are siblings of the button rather than its children, nested interactive elements (the TipTapEditor and its toolbar, selection controls, per-choice action buttons) keep working and stay valid HTML.
Behavior:
- The invisible button is reachable by Tab, shows a visible focus outline, and is activated by Enter/Space as well as by click.
- It carries an
aria-labeldescribing the action — "Edit question" for the prompt, "Edit answer option 2" for a choice card. - Activating it opens edit mode and moves focus into the
TipTapEditor. - While the region is closed, the inner
TipTapEditorreceivestabindex="-1"so the region's button is the single focus target rather than a competing one. - Once the region is open, a prop suppresses the button so it leaves the tab order entirely, and the editor becomes focusable again so the user can type and reach the toolbar.
- Clicking the region with a mouse behaves exactly as it does today.
Steps to reproduce
- Open the QTI demo page (the
/qti-demoroute in the channel editor). The first two cards are pre-populated with a Single Selection and a Multiple Selection question. - Click on the question prompt region with a mouse — it opens for editing. This is the behavior the
@clickhandler provides. - Reload the page so the region is closed again.
- Using only the keyboard, Tab through the card and attempt to open the prompt for editing. There is no focus stop that opens the region, and no Enter/Space activation on the prompt wrapper.
- Repeat for a choice card — same result:
.choice-borderresponds only to a mouse click. - Inspect the prompt wrapper and
.choice-borderelements in devtools and confirm neither carriestabindex,role, noraria-label.
Acceptance Criteria
ClickableRegion component
- A new
ClickableRegioncomponent renders a container with an invisible button that fills it, positioned behind the region's content; content renders as siblings of the button, never as children - The button's accessible name comes from a consumer-provided
aria-label - The button is reachable by Tab and activates on Enter, Space, and mouse click, emitting a single activation event in all three cases
- Keyboard focus produces a visible focus outline on the region; focus does not paint the hover background
- Hover state is driven by the button inside the component, with a default hover color living on the component for the common white-background case; call sites override it by passing a
styleset directly on the container (the prompt overrides toblue.v_100; choices override togreen.v_100orfineLinedepending on correctness) - A single prop suppresses the button entirely — not rendered, nothing in the tab order — covering both the already-open case and the not-clickable case (
mode !== 'edit') - Interactive content inside the region (the
TipTapEditorand its toolbar,KRadioButton/KCheckbox, per-choice action buttons) stays clickable and keyboard-reachable, with no interactive element nested inside the button
Adoption in ChoiceInteractionEditor.vue
- The prompt wrapper and each choice card use
ClickableRegioninstead of a bare@clickon a<div> - Mouse behavior is unchanged from today, including hover colors and the
cursor: pointertreatment only ineditmode - While a region is closed, its inner
TipTapEditorreceivestabindex="-1"; once open, the editor is focusable again so the user can type and reach the toolbar - Activating a region opens edit mode and moves focus into the
TipTapEditor - Accessible names are "Edit question" for the prompt and "Edit answer option 2" (per index) for choice cards, added as translatable strings in
qtiEditorStrings.js
Tests
- Unit tests for
ClickableRegioncover: button renders with the givenaria-label, activation via click and via Enter/Space, and the button being absent when suppressed - Tests in
ChoiceInteractionEditor.spec.jscover opening the prompt and opening a choice via keyboard
AI usage
This issue was drafted with Claude Code (Opus 4.8) from a code review of #6012. I described the problem and the intended structure; Claude read the code, asked clarifying questions, and drafted the sections. I reviewed and corrected the content — including the hover-color approach — before filing.

Activity