Skip to content

feat: introduce ClickableRegion component to improve keyboard accessibility for interaction editors - #6094

Open
Abhishek-Punhani wants to merge 1 commit into
learningequality:unstablefrom
Abhishek-Punhani:click-region
Open

feat: introduce ClickableRegion component to improve keyboard accessibility for interaction editors#6094
Abhishek-Punhani wants to merge 1 commit into
learningequality:unstablefrom
Abhishek-Punhani:click-region

Conversation

@Abhishek-Punhani

@Abhishek-Punhani Abhishek-Punhani commented Aug 17, 2026

Copy link
Copy Markdown
Member

Summary

Adding ClickableRegion component to improve keyboard accessibility for interaction editors

References

Closes #6043

Reviewer guidance

Navigate to the Qti-demo-page and and test navigating Choice Editor prompt/choice cards using keyboard (Tab, Enter/Space) to verify accessible aria-label announcements, focus outlines, and seamless interaction with nested controls (TipTap, selection inputs, action buttons) without triggering parent clicks.

AI usage

Used Antigravity for final review and nitpicks.

@Abhishek-Punhani

Copy link
Copy Markdown
Member Author

@AlexVelezLl, Should we add this to other interactions as well?

@AlexVelezLl

Copy link
Copy Markdown
Member

Hi @Abhishek-Punhani! Yes, let's use this for the textEntry prompt, and I just merged the ordering interaction PR; we can refactor that editor as well!

…bility for interaction editors

Signed-off-by: Abhishek-Punhani <punhani.manavabhi@gmail.com>

@AlexVelezLl AlexVelezLl left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @Abhishek-Punhani! It's really nice these button are finally accessible! I've found couple of things we might be able to simplify on the code.


<div
class="clickable-area"
@click="onClick"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The @click should be on the button element.

class="overlay-button"
:aria-label="ariaLabel"
@keydown.enter.prevent="onClick"
@keydown.space.prevent="onClick"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we add @click, we don't need to specify these two, as for button elements, the space and enter keystrokes also fire the onClick handler on a button element.

class="clickable-area"
@click="onClick"
>
<button

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is working great for keyboard navigation, but now for click interactions on prompts (this does not happen on options), we should have a slight background color change on hover, and we should have a cursor: pointer.

Grabacion.de.pantalla.2026-08-19.a.la.s.4.33.08.a.m.mov

<button
v-if="!suppressed"
type="button"
class="overlay-button"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, another small thing: the focus outline for this is blue-ish, but it should be the same color we have on $coreOutline

Outline color now:

Image

Correct outline color

Image

:minHeight="'80px'"
:autofocus="mode === 'edit' && isQuestionOpen"
:imageProcessor="EditorImageProcessor"
:tabindex="isQuestionOpen ? 0 : -1"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need these tab indexes; if it's open, tabindex should also be -1 because we can already navigate through the editor to the input. (i.e., it should always be -1, as we are already handling the tab index in the outer container).

Comment on lines 291 to 303
function handlePromptClick(event) {
if (props.mode !== 'edit') return;
if (event.target.closest('button') || event.target.closest('input')) return;
const closestBtn =
event.target && event.target.closest ? event.target.closest('button') : null;
if (closestBtn && !closestBtn.classList.contains('overlay-button')) return;
const closestInput =
event.target && event.target.closest ? event.target.closest('input') : null;
if (closestInput) return;
if (!isQuestionOpen.value) {
event.stopPropagation();
if (event && event.stopPropagation) event.stopPropagation();
openQuestion();
}
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need all of this? It's even less needed now because when the editor is already open, the Clickable region cannot fire any click event anymore, right?

return {
borderColor: questionHasError.value ? tokens.error : tokens.fineLine,
cursor: props.mode === 'edit' ? 'pointer' : undefined,
'--clickable-region-hover-bg': palette.blue.v_100,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I don't think we are using this anywhere.

Comment on lines +247 to +268
const closestBtn =
event.target && event.target.closest ? event.target.closest('button') : null;
if (closestBtn && !closestBtn.classList.contains('overlay-button')) return;
const closestInput =
event.target && event.target.closest ? event.target.closest('input') : null;
if (closestInput) return;
if (!isPromptOpen.value) {
event.stopPropagation();
if (event && event.stopPropagation) event.stopPropagation();
openPrompt();
}
}

function handleItemClick(event, itemId) {
if (props.mode !== 'edit') return;
if (openItemId.value === itemId) return;
if (event.target.closest('button') || event.target.closest('input')) return;
event.stopPropagation();
const closestBtn =
event.target && event.target.closest ? event.target.closest('button') : null;
if (closestBtn && !closestBtn.classList.contains('overlay-button')) return;
const closestInput =
event.target && event.target.closest ? event.target.closest('input') : null;
if (closestInput) return;
if (event && event.stopPropagation) event.stopPropagation();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

idem

Comment on lines 251 to +258
function handlePromptClick(event) {
if (props.mode !== 'edit') return;
if (event.target.closest('button') || event.target.closest('input')) return;
const closestBtn =
event.target && event.target.closest ? event.target.closest('button') : null;
if (closestBtn && !closestBtn.classList.contains('overlay-button')) return;
const closestInput =
event.target && event.target.closest ? event.target.closest('input') : null;
if (closestInput) return;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

idem

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[QTI] Choice editor clickable regions are mouse-only and lack accessible semantics

2 participants