Add keyboard controls for dragndrop#1308
Conversation
There was a problem hiding this comment.
Pull request overview
Adds keyboard-only interaction support to the Runestone DragNDrop interactive so learners can pick up a premise, navigate responses, and place/cancel via common keys, with accompanying visual focus/selection styling and Jest coverage.
Changes:
- Introduces
selectedPremisestate plus key handlers for selecting premises, navigating between premises/responses, placing, and canceling selection. - Updates tab order behavior so responses are only tabbable while a premise is selected.
- Adds CSS outlines for keyboard focus vs “picked up” state and adds a comprehensive keyboard-controls test suite.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| bases/rsptx/interactives/runestone/dragndrop/js/dragndrop.js | Implements keyboard interaction model (selection, focus movement, placement) and tab-order toggling. |
| bases/rsptx/interactives/runestone/dragndrop/test/dragndrop.test.js | Adds Jest tests covering the new keyboard behaviors and focus/tabindex expectations. |
| bases/rsptx/interactives/runestone/dragndrop/css/dragndrop.css | Adds visual styling for focus vs selected/picked-up premise state. |
Comments suppressed due to low confidence (1)
bases/rsptx/interactives/runestone/dragndrop/js/dragndrop.js:520
setDropListenersreferencesselfbut no longer defines it (the previous implicit globalself = thiswas removed). This will throw a ReferenceError on dragover/dragleave/drop for responses, breaking mouse/touch dragging.
setDropListeners(dpSpan) {
dpSpan.addEventListener(
"dragover",
function (ev) {
self.isAnswered = true;
ev.preventDefault();
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| dpSpan.addEventListener( | ||
| "dragover", |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.
Comments suppressed due to low confidence (1)
bases/rsptx/interactives/runestone/dragndrop/js/dragndrop.js:527
setDropListenersusesself.isAnswered = trueinside a handler that is already.bind(this). Hereselfresolves to the globalwindow.self, so the component’sisAnsweredflag won’t be updated correctly during pointer dragover.
"dragover",
function (ev) {
self.isAnswered = true;
ev.preventDefault();
if (ev.target.classList.contains("possibleDrop")) {
| this.isAnswered = true; | ||
| this.logBookEvent({ | ||
| event: "dragNdrop-drop", | ||
| div_id: this.divid, | ||
| act: `${premise.id} -> ${destinationName}`, |
There was a problem hiding this comment.
Resolved in followup commit.
3c51e83 to
93946fc
Compare
| dpSpan.addEventListener("dragleave", function (ev) { | ||
| self.isAnswered = true; | ||
| this.isAnswered = true; | ||
| ev.preventDefault(); | ||
| if (!ev.target.classList.contains("possibleDrop")) { | ||
| return; |
| /* Keep keyboard focus and the picked-up state visually distinct. */ | ||
| .draggable-drag:focus { | ||
| outline: 3px solid #3498db; | ||
| } | ||
|
|
||
| .draggable-drag.selected { | ||
| outline: 3px dashed #27ae60; | ||
| } |
| dpSpan.addEventListener("dragleave", function (ev) { | ||
| self.isAnswered = true; | ||
| this.isAnswered = true; | ||
| ev.preventDefault(); | ||
| if (!ev.target.classList.contains("possibleDrop")) { | ||
| return; |
| moveSelectedPremise(destination) { | ||
| const premise = this.selectedPremise; | ||
| if (!premise || premise.parentElement === destination) { | ||
| return; | ||
| } |
Implements keyboard controls allowing someone to complete a dragndrop without keyboard and mouse.
Mostly vibecoded. Interactions and basic aria attribute use verified by hand.