Skip to content

[QTI] Inline choice chip and Insert action in the passage editor #6181

Description

@AlexVelezLl

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

Overview

Build the inline choice chip: the TipTap node that represents one dropdown inside the passage, its node view, and the Insert action that places it. The chip is contributed to TipTapEditor from the QTI editor through the seams added in #6176, and TipTapEditor stays free of QTI vocabulary.

Complexity: High
Target branch: unstable

Context

In the design, the passage is a rich text editor with dropdowns inline as chips. Its toolbar shows history, clipboard, a prominent, labelled Insert button, and a More menu. The helper text reads "Inline choice widgets can be inserted blank or converted from highlighted text."

A chip shows:

  • a badge with its number of options
  • the text of its correct option as its label (e.g. 3 Moon), in a solid style
  • when it has no correct option: a dashed style with the label "Add answer" ("Add answers" when it has no options, e.g. 0 Add answers)
  • a selected state for the chip whose options are being edited below the passage

#6176 adds the pieces this builds on: the extensions prop, the @ready event, insertActions descriptors with a prominent desktop treatment, the selection / canInsertNode facts, the transformPasted guidance, and reading consumer state from a node view via provide / inject.

A chip's options travel with the chip. Undo and redo restore the passage document, and copy / paste moves document content. If a chip's options and correct answer lived only in the interaction's Vue state, undoing a chip deletion would bring back an empty chip, and a pasted chip would arrive with no options. So the node carries its dropdown's data, and the editor UI edits it through the node.

The Change

  • An inline choice node and node view under interactions/inlineChoice/, registered on the passage's TipTapEditor through the extensions prop. The node is an inline atom whose attributes hold its dropdown's response identifier, options and correct answer.
  • The node parses from, and renders to, the passage markup [QTI] Inline choice interaction: descriptor, parsing, XML assembly and validation #6180 defines, so passage HTML round-trips through the editor without losing a dropdown or its data.
  • The node view renders the chip as described above, and shows it as selected when it is the dropdown the options list is editing (read through provide / inject). Clicking or keyboard-selecting a chip makes it the selected dropdown.
  • An Insert action contributed through insertActions, with the prominent (labelled) desktop treatment:
    • with an empty selection, it inserts a blank chip (no options) at the cursor and selects it
    • with text highlighted within one paragraph, it replaces that text with a chip whose single option is that text, marked correct, and selects it. The passage is rich text but options are plain text, so any styling in the selection (bold, italic, links, etc.) is dropped and only its text is kept
    • it is unavailable when the selection spans more than one block (selection.spansBlocks), or where the node cannot be inserted (canInsertNode)
  • A pasted or duplicated chip gets a fresh response identifier and fresh choice identifiers (in transformPasted), so an item never carries two dropdowns with the same identifier. Cut and paste within the same passage should keep the chip's identity, since it is a move rather than a copy.

Out of Scope

Acceptance Criteria

General

  • Insert with an empty selection places a blank chip (0 Add answers, dashed) at the cursor and selects it
  • Insert over highlighted text within one paragraph replaces it with a chip that has that text as its one option, marked correct, and selects it
  • Insert over styled text (e.g. bold, italic, a link) produces an option with the same text and no styling
  • Insert is unavailable when the selection spans more than one paragraph
  • A chip shows its option count and its correct option's text, and switches to the dashed "Add answer" style when it has no correct option
  • Undoing the deletion of a chip restores it with all its options and its correct answer
  • Copying and pasting a chip produces a second chip with the same options and a different response identifier
  • Passage HTML containing chips round-trips through the editor unchanged
  • No file under shared/views/TipTapEditor imports from or refers to the QTI editor

Accessibility and i18n

  • Insert is part of the toolbar's roving tabindex and is keyboard-operable
  • A chip can be reached and selected with the keyboard
  • A chip has an accessible name that says it is an answer dropdown, with its option count and its correct answer, or that it has none yet
  • The chip's labels ("Add answer", "Add answers", accessible names) and the Insert label go through qtiEditorStrings
  • Chips lay out correctly in RTL

Testing

  • Tests cover both Insert paths and its unavailability across paragraphs
  • Tests cover undo after deleting a chip, and paste giving a fresh response identifier
  • A test covers the chip's accessible name in each state

Testing

pnpm jest --config jest_config/jest.conf.js contentcuration/contentcuration/frontend/shared/views/QTIEditor/interactions/inlineChoice

References

AI usage

I used Claude Code to draft this issue from the Figma design and #6176, which it read alongside the existing TipTap extensions. I decided how to split the work and reviewed and edited the text.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions