Skip to content

Product Issue: Gap Match QTI Interaction #15213

Description

@habibayman

This issue is not open for contribution. Visit Contributing guidelines to learn about the contributing process and how to find suitable issues.

Overview

Learners interacting with QTI assessments that include a gap match interaction see a response pool of words or phrases alongside a passage with blanks embedded directly in its text; dragging a response into a blank fills it. Unlike associate and match, the targets aren't a flat list or a set of rows — they're gaps sitting arbitrarily deep inside authored passage content (a blockquote, a table cell), and the passage's surrounding text must render intact around them. The design also covers a second mode, answer-specific distractor pools, where only a subset of the pool is valid for a given gap. Rendering supports all QTI display variations, including those for assessments created outside of Kolibri Studio.

Description and outcomes

This issue covers the learner-facing rendering of qti-gap-match-interaction in the Kolibri QTI viewer. Two modes are both in scope:

  • A learner presented with a gap match interaction sees a response pool alongside a passage with one or more blanks embedded inline in its text, wherever they're authored — including inside a blockquote or a table cell — with the surrounding sentence rendering intact
  • Dragging a response into an empty, compatible gap fills it
  • A response with match-max="0" (unlimited) stays visible and active in the pool after being placed, filling multiple gaps; a response with a finite match-max is disabled once its uses are spent.
  • A learner without pointer/drag ability can select a pool response, then select a gap, to place it (click-only path); selecting a filled gap clears it
  • A learner can complete the interaction via keyboard: tabbing reaches a gap directly, arrow
    keys cycle through that gap's currently-valid responses, tab commits and advances
  • Single answer pool (default): every response in the pool is a valid candidate for every gap
  • Answer-specific distractor pools: not every response is valid for every gap. Selecting a gap highlights only the pool responses valid for it; dragging a response shows the gaps that would accept it as live drop targets and visually disables the gaps that would reject it
  • Multiple gap match interactions can appear within the same item body, each binding only its own gap identifiers even when two interactions reuse the same identifier names

Resources

Accessibility Requirements

  • Each gap exposes an accessible name describing its state and position: "Gap {number} of {total}: empty" or "Gap {number} of {total}: {response}"
  • Keyboard operation reuses the shared listbox pattern extracted from Associate/Match: tab reaches
    a gap directly, arrow keys cycle that gap's currently-valid responses, tab commits and advances
  • A response incompatible with the active gap simply does not appear in that gap's option cycle —
    silent, the same treatment already used when a total-cap limit is reached; no spoken refusal is
    added for this case, since the design shows no message for it
  • aria-disabled="true" is set on gaps rendered as invalid drop targets during a drag
  • Drag-and-drop has a fully equivalent non-drag path (click-only), not just a keyboard alternative
  • RTL layout is supported

Core interactions

Description Screenshot
Single answer pool: every response is valid for every gap Image
Answer-specific distractor pools — click: selecting a gap highlights only its valid responses Image
Answer-specific distractor pools — drag: dragging a response shows valid gaps live and disables invalid ones Image

Acceptance Criteria

  • Gaps are located by walking the passage content to arbitrary depth, not just the top level of the interaction's content — gaps inside a blockquote or a table cell are found and bound correctly, in document order
  • Learner can drag a pool response into an empty, compatible gap; the gap becomes filled
  • Click-only path: selecting a pool response then a gap places it, equivalent to drag; selecting a filled gap clears it
  • Keyboard operation reuses the shared listbox composable
  • Answer-specific distractor pools: a response and a gap are compatible unless either side
    names a match-group that excludes the other, per the QTI attribute's actual semantics (a
    space-separated identifier list on the choice, not a shared label) — implemented as a single
    predicate consumed identically by the keyboard candidate list, the click-highlight state, and
    the drag accepts check, so the three paths can't independently disagree
  • With no match-group authored anywhere, every response is compatible with every gap — the
    unrestricted case is the restricted case with a trivial predicate, not a separate code path
  • The response pool shuffles when the interaction's shuffle attribute is set; the passage and
    gap order are never shuffled
  • max-associations enforces a total cap on filled gaps across the whole interaction,
    defaulting to 1 per the QTI spec default for this interaction, independent of any individual
    response's own reuse limit
  • qti-input-width-N sizes a gap's blank to approximately N characters as a minimum width, so
    a long response never breaks the surrounding line
  • RTL rendering uses logical CSS properties

Out of scope for this issue

  • qti-graphic-gap-match-interaction (image + hotspot variant) — different content model and a11y pattern, tracked separately
  • qti-choices-top/bottom/left/right, data-choices-container-width, data-max/min-selections-message — stay no-ops, consistent with Match and Order
  • Mobile view

AI usage

Used Claude to help phrase the design decisions and behavior when drafting this issue from the
Figma design and from patterns established while building the associate and match interactions.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions