You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
❌ 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:
<divclass="container"><buttonclass="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-label describing 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 TipTapEditor receives tabindex="-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-demo route 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 @click handler 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-border responds only to a mouse click.
Inspect the prompt wrapper and .choice-border elements in devtools and confirm neither carries tabindex, role, nor aria-label.
Acceptance Criteria
ClickableRegion component
A new ClickableRegion component 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 style set directly on the container (the prompt overrides to blue.v_100; choices override to green.v_100 or fineLine depending 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 TipTapEditor and 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 ClickableRegion instead of a bare @click on a <div>
Mouse behavior is unchanged from today, including hover colors and the cursor: pointer treatment only in edit mode
While a region is closed, its inner TipTapEditor receives tabindex="-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 ClickableRegion cover: button renders with the given aria-label, activation via click and via Enter/Space, and the button being absent when suppressed
Tests in ChoiceInteractionEditor.spec.js cover 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.
❌ This issue is not open for contribution. Visit Contributing guidelines to learn about the contributing process and how to find suitable issues.
Target branch:
unstableObserved behavior
The QTI Choice editor makes a whole region clickable to enter edit mode by attaching a bare
@clickhandler to a plain<div>. This happens in two places inChoiceInteractionEditor.vue:@click="handlePromptClick").choice-border(@click="handleChoiceClick")Neither container carries any interactive semantics — no
tabindex, norole="button", noaria-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 — theTipTapEditorand its toolbar, theKRadioButton/KCheckboxselection 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
ClickableRegioncomponent provides this. It renders an invisible button that fills the container and sits behind the region's content:Because the content are siblings of the button rather than its children, nested interactive elements (the
TipTapEditorand its toolbar, selection controls, per-choice action buttons) keep working and stay valid HTML.Behavior:
aria-labeldescribing the action — "Edit question" for the prompt, "Edit answer option 2" for a choice card.TipTapEditor.TipTapEditorreceivestabindex="-1"so the region's button is the single focus target rather than a competing one.Steps to reproduce
/qti-demoroute in the channel editor). The first two cards are pre-populated with a Single Selection and a Multiple Selection question.@clickhandler provides..choice-borderresponds only to a mouse click..choice-borderelements in devtools and confirm neither carriestabindex,role, noraria-label.Acceptance Criteria
ClickableRegioncomponentClickableRegioncomponent 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 childrenaria-labelstyleset directly on the container (the prompt overrides toblue.v_100; choices override togreen.v_100orfineLinedepending on correctness)mode !== 'edit')TipTapEditorand its toolbar,KRadioButton/KCheckbox, per-choice action buttons) stays clickable and keyboard-reachable, with no interactive element nested inside the buttonAdoption in
ChoiceInteractionEditor.vueClickableRegioninstead of a bare@clickon a<div>cursor: pointertreatment only ineditmodeTipTapEditorreceivestabindex="-1"; once open, the editor is focusable again so the user can type and reach the toolbarTipTapEditorqtiEditorStrings.jsTests
ClickableRegioncover: button renders with the givenaria-label, activation via click and via Enter/Space, and the button being absent when suppressedChoiceInteractionEditor.spec.jscover opening the prompt and opening a choice via keyboardAI 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.