Skip to content

Hparsons accessibility - #1473

Open
ascholerChemeketa wants to merge 5 commits into
RunestoneInteractive:mainfrom
ascholerChemeketa:hparsons-accessibility
Open

Hparsons accessibility#1473
ascholerChemeketa wants to merge 5 commits into
RunestoneInteractive:mainfrom
ascholerChemeketa:hparsons-accessibility

Conversation

@ascholerChemeketa

@ascholerChemeketa ascholerChemeketa commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Screenreader/keyboard navigation overhaul for hparsons


This pull request introduces significant accessibility improvements and keyboard navigation enhancements to the Parsons block interactive and drag-and-drop components. The main themes are improved ARIA support for screen readers, a new unified keyboard movement mode for arranging blocks, and fixes for image drag behavior. These changes make the interactive components more accessible and user-friendly, especially for users relying on keyboard navigation or assistive technology.

Accessibility and ARIA Improvements:

  • Added a live region (feedbackLiveRegion) to BlockFeedback for polite screen reader feedback, and ensured feedback is announced or cleared at appropriate times. [1] [2] [3] [4]
  • Improved ARIA attributes and roles in ParsonsInput, including per-block aria-labels, aria-selected, and aria-activedescendant management, and live keyboard instructions. [1] [2]
  • Added a MutationObserver in HParsons to ensure MathJax elements do not disrupt tab order, and improved math block rendering accessibility. [1] [2]

Keyboard Navigation Enhancements:

  • Replaced per-block tab stops with a single tab stop for the entire Parsons input, introducing a keyboard movement mode activated by Enter/Space. Users can now move blocks using arrow keys and Enter, with clear instructions and ARIA updates. [1] [2]

Supporting Methods and API:

  • Exposed a refreshBlockAria method on the Parsons input and element to allow parent components to trigger ARIA updates as needed. [1] [2]

These changes collectively make the Parsons and drag-and-drop interactives more robust, accessible, and maintainable.

Copilot AI lite review requested due to automatic review settings September 3, 2026 18:59
@ascholerChemeketa

Copy link
Copy Markdown
Contributor Author

Ooops, was based off another branch. Just rebased off of main.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

The new keyboard movement mode can leave _activeBlock null (and aria-activedescendant stale) while still in role="application", causing runtime errors and incorrect accessibility state.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

This PR improves accessibility and keyboard navigation for the HParsons (micro-parsons) interactive and fixes an image-related drag-and-drop edge case, with accompanying tests.

Changes:

  • Adds persistent screen-reader feedback announcements (live region) and refreshes per-block ARIA state after grading/clearing/reset.
  • Reworks ParsonsInput to use a single Tab stop with an “activation” movement mode (Enter/Space) and updated ARIA labeling/selection semantics.
  • Ensures MathJax-rendered content does not introduce tab stops by disabling tabindex and observing late DOM mutations; prevents images inside draggable premises from becoming the drag target.
File summaries
File Description
bases/rsptx/interactives/runestone/hparsons/test/hparsons.test.js Adds tests for MathJax tab-stop removal, live region feedback announcements, and keyboard movement mode behavior.
bases/rsptx/interactives/runestone/hparsons/js/micro-parsons/ParsonsInput.ts Introduces unified keyboard movement mode, active-block tracking, and richer per-block ARIA updates.
bases/rsptx/interactives/runestone/hparsons/js/micro-parsons/micro-parsons.ts Exposes refreshBlockAria() on the custom element API.
bases/rsptx/interactives/runestone/hparsons/js/hparsons.js Adds MathJax tabindex cleanup + MutationObserver and makes math rendering return a Promise.
bases/rsptx/interactives/runestone/hparsons/js/BlockFeedback.js Adds a persistent live region for grading/reset announcements and refreshes block ARIA after feedback changes.
bases/rsptx/interactives/runestone/dragndrop/test/dragndrop.test.js Adds a regression test ensuring dragging an image uses the enclosing premise.
bases/rsptx/interactives/runestone/dragndrop/js/dragndrop.js Disables image draggability within premises and uses currentTarget for correct drag/drop element IDs.
Review details
  • Files reviewed: 5/5 changed files
  • Comments generated: 2
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +445 to +450
if (
this.el.getAttribute("role") === "application" &&
this._activeBlock
) {
this.el.setAttribute("aria-activedescendant", this._activeBlock.id);
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed

Comment on lines +552 to +556
const activeBlock = this._activeBlock as HTMLDivElement;
const currentArea =
activeBlock.parentElement === this._dragArea
? this._dragArea
: this._dropArea;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed

Copilot AI review requested due to automatic review settings September 3, 2026 19:03

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

There are confirmed runtime/behavioral issues (stale live-region announcements via uncanceled timeouts, misleading Promise semantics in renderMathInBlocks(), and a potential null dereference in keyboard movement mode) that should be addressed before approval.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Suppressed comments (1)

bases/rsptx/interactives/runestone/hparsons/js/micro-parsons/ParsonsInput.ts:556

  • In keyboard movement mode, the keydown handler assumes _activeBlock is always non-null (as HTMLDivElement). But _updateBlockAria can set _activeBlock = null if the active block is removed/re-rendered while role is still application, which would cause a runtime error on the next key press when accessing activeBlock.parentElement. Add a null/containment guard and exit movement mode (or pick a fallback block) when _activeBlock is missing.
      const activeBlock = this._activeBlock as HTMLDivElement;
      const currentArea =
        activeBlock.parentElement === this._dragArea
          ? this._dragArea
          : this._dropArea;
  • Files reviewed: 5/5 changed files
  • Comments generated: 2
  • Review effort level: Lite

Comment on lines +171 to +175
announceFeedback(message = this.messageDiv.textContent.trim()) {
this.feedbackLiveRegion.textContent = "";
setTimeout(() => {
this.feedbackLiveRegion.textContent = message;
}, 10);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in followup commit

Comment on lines +228 to +245
setTimeout(() => {
const blocks = this.hparsonsInput.querySelectorAll(
".parsons-block",
);
blocks.forEach((block) => {
block.innerHTML = this.decodeHTMLEntities(block.innerHTML);
if (block.innerHTML.indexOf("process-math") !== -1) {
block.innerHTML = block.innerHTML.replace(
/<span class="process-math">|<\/span>/g,
"",
);
}
this.queueMathJax(block);
});
disableMathJaxTabStops(this.hparsonsInput, [".parsons-block"]);
resolve();
}, 10);
});

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in followup commit

Copilot AI review requested due to automatic review settings September 3, 2026 19:24

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

The new keyboard movement mode has confirmed correctness issues (duplicate IDs from clones, potential runtime crash when _activeBlock is null, and focus/ARIA inconsistencies) that should be fixed before merging.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details

Suppressed comments (5)

Previously missed (3) — in code that hasn't changed since the last review.

bases/rsptx/interactives/runestone/hparsons/js/micro-parsons/ParsonsInput.ts:430

  • Blocks can be cloned (reusable mode / Sortable clone) and will carry over an existing id, which can create duplicate IDs in the DOM. That breaks aria-activedescendant targeting and violates the uniqueness requirement for element IDs. Update the ID assignment to also regenerate IDs when document.getElementById(block.id) refers to a different node (duplicate case).
    bases/rsptx/interactives/runestone/hparsons/js/micro-parsons/ParsonsInput.ts:466
  • aria-activedescendant is being maintained on this.el, but _setActiveBlock() moves DOM focus onto the block itself (block.focus()). The aria-activedescendant pattern is only honored when focus remains on the owning element, so screen readers may ignore the active-descendant updates. Consider either (a) keeping focus on this.el and relying on aria-activedescendant, or (b) dropping aria-activedescendant and relying on the focused block.
    bases/rsptx/interactives/runestone/hparsons/js/micro-parsons/ParsonsInput.ts:547
  • While in keyboard movement mode, pressing Escape exits the mode but leaves focus on the last focused block (tabindex=-1). That undermines the “single Tab stop” goal because focus can remain on a non-tabbable descendant. After handling Escape, move focus back to the keyboard surface element (this.el).

bases/rsptx/interactives/runestone/hparsons/js/micro-parsons/ParsonsInput.ts:556

  • this._activeBlock can become null (e.g., if blocks are reset/removed while movement mode is active). The keydown handler then casts it to HTMLDivElement and immediately dereferences parentElement, which will throw at runtime. Guard against a missing active block before using it.
      const activeBlock = this._activeBlock as HTMLDivElement;
      const currentArea =
        activeBlock.parentElement === this._dragArea
          ? this._dragArea
          : this._dropArea;

bases/rsptx/interactives/runestone/hparsons/js/micro-parsons/ParsonsInput.ts:449

  • In movement mode, aria-activedescendant is only set when _activeBlock is truthy, but when _activeBlock becomes null (e.g., if the active block is removed), the previous aria-activedescendant value is left behind and becomes stale. Clear aria-activedescendant when there is no active block.
    if (
      this.el.getAttribute("role") === "application" &&
      this._activeBlock
    ) {
      this.el.setAttribute("aria-activedescendant", this._activeBlock.id);
  • Files reviewed: 6/6 changed files
  • Comments generated: 1
  • Review effort level: Lite

Comment thread bases/rsptx/interactives/runestone/hparsons/js/hparsons.js
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings September 3, 2026 21:24

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 Needs a closer look

There are confirmed keyboard-mode robustness and activation issues in ParsonsInput (null active block handling, stale aria-activedescendant, and unreliable click activation) that can cause runtime errors or broken accessibility behavior.

Review details

Suppressed comments (6)

Previously missed (5) — in code that hasn't changed since the last review.

bases/rsptx/interactives/runestone/hparsons/js/micro-parsons/ParsonsInput.ts:435

  • aria-label for each block is built from _getTextFromBlock(...), which won’t pick up MathJax’s rendered speech text (e.g., mjx-container[aria-label]). For math blocks this can result in empty/incorrect labels even though MathJax provides an accessible label. Prefer MathJax’s aria-label when present, with _getTextFromBlock as a fallback.
    bases/rsptx/interactives/runestone/hparsons/js/micro-parsons/ParsonsInput.ts:450
  • _updateBlockAria sets aria-activedescendant when _activeBlock is present, but never clears it when _activeBlock becomes null while the surface remains in role="application" (e.g., after a reset removes the active block). This can leave a stale aria-activedescendant pointing at a non-existent element.
    bases/rsptx/interactives/runestone/hparsons/js/micro-parsons/ParsonsInput.ts:531
  • The click-to-activate handler only triggers when ev.target === this.el. Since the container has multiple child elements (tips/areas), clicks will usually target a child node and never activate keyboard movement, even though the surface has role="button". Consider activating on any click that’s not on a .parsons-block instead of requiring the click target to be the root element.
    bases/rsptx/interactives/runestone/hparsons/js/micro-parsons/ParsonsInput.ts:556
  • In keyboard movement mode, the keydown handler dereferences _activeBlock via a type cast without a null check. If _activeBlock is cleared (e.g., reset removes the focused block), arrow/enter handling will throw when reading activeBlock.parentElement. Add a guard to re-establish an active block (or exit movement mode) when _activeBlock is missing.
    bases/rsptx/interactives/runestone/hparsons/js/micro-parsons/ParsonsInput.ts:86
  • The SR-only instructions say "Press Enter" but the key handler also allows Space (ev.key === " ") to activate keyboard movement. Update the instructions to mention Space so screen reader users get accurate guidance.

This issue also appears on line 495 of the same file.

bases/rsptx/interactives/runestone/hparsons/js/micro-parsons/ParsonsInput.ts:496

  • The SR-only instructions are reset on exit to "Press Enter...", but Space also activates movement mode. Keep the exit-state instructions consistent with the actual supported keys.
    this._keyboardInstructions.textContent =
      "Press Enter to move blocks with the keyboard.";
  • Files reviewed: 6/6 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

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.

2 participants